# How to Handle Errors Gracefully in Medusa API Routes

> Learn to handle errors gracefully in Medusa API routes. Centralized middleware maps MedusaError and Zod validation to HTTP codes for standardized JSON responses.

- Repository: [Medusa/medusa](https://github.com/medusajs/medusa)
- Tags: how-to-guide
- Published: 2026-05-19

---

**Medusa handles errors gracefully by routing all thrown `MedusaError` instances and Zod validation failures through a centralized error-handler middleware that automatically maps error types to HTTP status codes and returns standardized JSON responses.**

Medusa's HTTP layer separates error generation from error delivery through a layered architecture that eliminates repetitive try-catch blocks in individual routes. When working with the `medusajs/medusa` codebase, you throw domain-specific errors from services or workflows and let the core framework transform them into consistent HTTP responses. This approach ensures that every API endpoint returns predictable error payloads without cluttering route handlers with boilerplate error-handling logic.

## Understanding Medusa's Layered Error Architecture

Medusa employs a three-stage pipeline for error management: generation, formatting, and delivery. Domain logic throws `MedusaError` objects, the framework normalizes and enriches these exceptions, and a centralized middleware sends the final HTTP response. This separation keeps your API routes clean while guaranteeing uniform error semantics across the entire application.

### Error Generation with MedusaError

Domain errors originate in services, repositories, or workflow steps using the `MedusaError` class from `@medusajs/utils`. The constructor accepts a `type` from the `MedusaError.Types` enum (`NOT_FOUND`, `INVALID_DATA`, `CONFLICT`, `UNEXPECTED_STATE`, etc.) and a descriptive message.

```ts
import { MedusaError } from "@medusajs/utils"

// Example in a service method
if (!order) {
  throw new MedusaError(
    MedusaError.Types.NOT_FOUND,
    `Order with id ${orderId} not found`
  )
}

```

Validation errors follow a similar path. When Zod validators fail, the framework automatically transforms the resulting `ZodError` into a `MedusaError` before it reaches your route handler.

### The Centralized Error-Handler Middleware

The core error-handling logic lives in [`packages/core/framework/src/http/middlewares/error-handler.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/http/middlewares/error-handler.ts) (lines 44-71). This middleware performs several critical functions:

- **Scope Resolution**: Retrieves the Medusa logger from the request scope using `req.scope.resolve(ContainerRegistrationKeys.LOGGER)` or falls back to `console`
- **Exception Formatting**: Calls `formatException(err)` to normalize any thrown value into a `MedusaError` structure
- **Status-Code Mapping**: Switches on `err.type` and `err.name` to select appropriate HTTP status codes (e.g., `404` for `NOT_FOUND`, `400` for `INVALID_DATA`, `409` for `CONFLICT`)
- **Intelligent Logging**: Logs errors at the *error* level for 5xx status codes and *info* level for client errors (4xx)
- **Zod Issue Processing**: If the error contains an `issues` array, each issue is converted to a human-readable message via `fromZodIssue` and returned as a 400 Bad Request payload
- **JSON Response**: Sends a standardized payload matching the OpenAPI `Error` schema: `{ code, type, message }`

A thin wrapper re-exports this middleware for the main `medusa` package at [`packages/medusa/src/utils/middlewares/error-handler.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/utils/middlewares/error-handler.ts).

## Implementing Error Handling in API Routes

API routes in Medusa are plain async functions that do not require explicit try-catch blocks. The Express application registers the error handler via `app.use(errorHandler())` in [`packages/core/framework/src/http/router.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/http/router.ts), ensuring all thrown errors bubble up to the centralized handler.

### Automatic Error Propagation

Consider the admin orders route at [`packages/medusa/src/api/admin/orders/route.ts`](https://github.com/medusajs/medusa/blob/main/packages/medusa/src/api/admin/orders/route.ts). The handler invokes a workflow that may throw `MedusaError` if resources are missing:

```ts
// packages/medusa/src/api/admin/orders/route.ts
export const GET = async (
  req: AuthenticatedMedusaRequest<HttpTypes.AdminOrderFilters>,
  res: MedusaResponse<HttpTypes.AdminOrderListResponse>
) => {
  const variables = {
    filters: { ...req.filterableFields, is_draft_order: false },
    ...req.queryConfig.pagination,
  }

  const workflow = getOrdersListWorkflow(req.scope)
  const { result } = await workflow.run({
    input: { fields: req.queryConfig.fields, variables },
  })

  const { rows, metadata } = result as { rows: OrderDTO[]; metadata: any }

  res.json({
    orders: rows as unknown as HttpTypes.AdminOrder[],
    count: metadata.count,
    offset: metadata.skip,
    limit: metadata.take,
  })
}

```

If the workflow throws `new MedusaError(MedusaError.Types.NOT_FOUND, "Order not found")`, the middleware automatically responds with:

```json
{
  "code": "not_found",
  "type": "not_found",
  "message": "Order not found"
}

```

### Optional Custom Error Enrichment

When you need to add context before the global handler processes an error, catch the exception, enrich it, and re-throw:

```ts
import { MedusaError } from "@medusajs/utils"

export const DELETE = async (req, res) => {
  try {
    await someService.delete(req.params.id)
    res.status(200).json({ success: true })
  } catch (e) {
    // Convert unknown errors to a generic internal error
    const err = e instanceof MedusaError
      ? e
      : new MedusaError(MedusaError.Types.UNEXPECTED_STATE, "Unexpected failure")

    // Attach extra context for debugging
    err.message = `Failed to delete ${req.params.id}: ${err.message}`
    throw err   // Delegate to core middleware for final response formatting
  }
}

```

## Status Code Mapping Reference

The error-handler middleware maps `MedusaError.Types` to HTTP status codes as implemented in [`packages/core/framework/src/http/middlewares/error-handler.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/http/middlewares/error-handler.ts):

- **`NOT_FOUND`** → `404 Not Found`
- **`INVALID_DATA`** → `400 Bad Request`
- **`CONFLICT`** → `409 Conflict`
- **`UNAUTHORIZED`** → `401 Unauthorized`
- **`NOT_ALLOWED`** → `403 Forbidden`
- **`UNEXPECTED_STATE`** → `500 Internal Server Error` (catch-all for system failures)

Zod validation errors automatically map to `400 Bad Request` with detailed issue arrays.

## Summary

- Throw `MedusaError` from services, repositories, and workflows using specific types like `NOT_FOUND` or `INVALID_DATA` to trigger appropriate HTTP responses
- The centralized middleware at [`packages/core/framework/src/http/middlewares/error-handler.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/http/middlewares/error-handler.ts) handles all formatting, logging, and JSON serialization automatically
- API routes require no explicit try-catch blocks; errors bubble up from any layer of the application and reach the global handler via Express middleware registration
- Zod validation errors are automatically transformed into `400 Bad Request` responses with human-readable messages via the exception formatter
- For advanced scenarios, catch errors in your route handler to enrich context, then re-throw to maintain the standardized error response format

## Frequently Asked Questions

### What happens if I throw a standard JavaScript Error instead of MedusaError?

The middleware's `formatException` function normalizes unknown errors into `MedusaError` instances with type `UNEXPECTED_STATE`. While the request will still fail with a `500` status code, you lose the semantic precision and proper status-code mapping that explicit `MedusaError` types provide. Always use `MedusaError` for domain-specific failures to ensure consistent API contracts.

### How does Medusa convert Zod validation errors into HTTP responses?

Zod errors are intercepted by the exception formatter ([`packages/core/framework/src/http/middlewares/exception-formatter.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/http/middlewares/exception-formatter.ts)) and transformed into `MedusaError` instances with type `INVALID_DATA`. The error handler then maps these to `400 Bad Request` responses, using `fromZodIssue` to convert each Zod issue into a human-readable message in the response payload.

### Can I customize the HTTP status code for a specific error scenario?

The status-code mapping is hardcoded in the core error-handler middleware. To return a custom status code, catch the error in your route handler, inspect the condition, and throw a different `MedusaError` type that maps to your desired code, or manually send the response and terminate the request. Re-throwing ensures you maintain the standardized JSON payload structure defined by the framework.

### Where is the error-handler middleware registered in the Medusa application lifecycle?

The middleware is registered via `app.use(errorHandler())` within the core HTTP router initialization at [`packages/core/framework/src/http/router.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/http/router.ts). This registration occurs before route definitions, ensuring that all admin and store API endpoints pass through the centralized error handling logic before returning responses to the client.