Throwing and Handling Errors

In this guide, you'll learn how to return errors in your MedusaGo application, and how they affect an API route's response.

Return a MedusaError#

When an error occurs in your API routes, middlewares, workflows, or any customization, return a *MedusaError from the utils/src/common package. Go has no exceptions, so return the error from your function instead of throwing it. A handler or middleware that returns an error stops the request.

The MedusaGo application's API route error handler then wraps your returned error in a uniform object and returns it in the response.

For example:

src/api/custom/search/route.go
// searchGET returns an INVALID_DATA error when the q query parameter is
// missing. The error handler turns it into a 400 response.
func searchGET(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	q, _ := queryString(req.Query, "q")
	if q == "" {
		return common.NewMedusaError(
			common.MedusaErrorTypes.INVALID_DATA,
			"The `q` query parameter is required.",
		)
	}

	return res.JSON(map[string]any{"q": q})
}

The NewMedusaError function accepts the following parameters:

  1. The error's type. The common.MedusaErrorTypes variable has the available types, which are explained in the next section.
  2. The message to show in the error response.
  3. Optionally, a code to return in the response, as a third parameter.
Tip: Return errors from your functions, and don't panic. A panic in a route handler isn't passed to the error handler: it doesn't produce an error response. Use common.AsMedusaError to check if an error you received is a MedusaError, and read its Type.

Error Object in Response#

The error object returned in the response has three properties:

  • type: The error's type.
  • message: The error message, if available.
  • code: A common snake-case code. Its values can be:
    • invalid_request_error for the DUPLICATE_ERROR type.
    • api_error for the DB_ERROR type.
    • invalid_state_error for the CONFLICT error type.
    • unknown_error for any unidentified error type.
    • For other error types, this property won't be available unless you provide a code as a third parameter to NewMedusaError.

For example, this API route returns an error with a code:

src/api/custom/quota/route.go
func quotaGET(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	// The third argument is an optional code, returned as the code property.
	return common.NewMedusaError(
		common.MedusaErrorTypes.NOT_ALLOWED,
		"The quota was reached.",
		"quota_reached",
	)
}

It responds with a 400 status code and the following body:

Code
{
  "code": "quota_reached",
  "type": "not_allowed",
  "message": "The quota was reached."
}

MedusaError Types#

Type

Description

Status Code

DB_ERROR

Indicates a database error.

500

DUPLICATE_ERROR

Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.

422

INVALID_ARGUMENT and UNEXPECTED_STATE

Indicates an error that occurred due to incorrect arguments or other unexpected state.

500

INVALID_DATA

Indicates a validation error.

400

UNAUTHORIZED

Indicates that a user is not authorized to perform an action or access a route.

401

FORBIDDEN

Indicates that a user is authenticated, but isn't allowed to perform an action.

403

NOT_FOUND

Indicates that the requested resource, such as a route or a record, isn't found.

404

NOT_ALLOWED

Indicates that an operation isn't allowed.

400

CONFLICT

Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored in favor of a default message. Refer to the note below for details.

409

PAYMENT_AUTHORIZATION_ERROR

Indicates an error has occurred while authorizing a payment.

422

Other error types, and errors that aren't a MedusaError

Any other error type results in an unknown_error code and the message An unknown error occurred.. The error is logged with its details.

500

Conflict Error Messages#

The error handler replaces the message of a CONFLICT error with the following default message:

Code
The request conflicted with another request. You may retry the request with the provided Idempotency-Key.

So, only return a CONFLICT error for internal conflicts, such as a request that conflicts with an ongoing transaction or a lock that another process holds. The client doesn't need details about these conflicts, and the default message avoids exposing internal implementation details.

Tip: If the client needs the details of the conflict, return a NOT_ALLOWED error instead. Its message reaches the client, but the response has a 400 status code rather than 409.

Database Errors#

Errors from the database layer that the application recognizes, such as a unique constraint violation or a reference to a record that doesn't exist, are converted into a MedusaError before the response is sent. For example, a unique constraint violation results in a DUPLICATE_ERROR.


Override Error Handler#

Not supported: Medusa lets you replace the error handler of the application. MedusaGo doesn't have an option for that yet: every error is handled by the error handler described in this chapter.

If you need to run custom logic when an error occurs, such as capturing it in a third-party service, handle the error where it's returned. For example, wrap your route handlers in a function that reports the errors they return before it returns them:

src/api/errors.go
// withErrorReporting wraps a route handler: it reports the errors the
// handler returns, then returns them for the error handler to process.
func withErrorReporting(handler medusahttp.RouteHandler) medusahttp.RouteHandler {
	return func(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
		err := handler(req, res)
		if err != nil {
			// for example, capture the error in a third-party service
			slog.Error("route failed", "path", req.Path(), "error", err)
		}
		return err
	}
}
Found a problem with this page?Report Issue
Edited Nov 14·Edit this page