> ## Documentation Index
> Fetch the complete documentation index at: https://wemstudios.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling in Blaze: BlazeError and app.onError

> Blaze uses Express-style 4-argument error handlers. Throw BlazeError for structured HTTP errors, or use app.onError() for a global error boundary.

Blaze follows the Express 4-argument error handler convention throughout. When any middleware or route handler throws an exception, or calls `next(err)` with a non-falsy value, the error skips all remaining normal handlers and flows to the nearest **downstream error handler** — a function with the signature `(err, req, res, next)`. You can register one global error handler on the app, one per sub-router, or both.

***

## BlazeError

`BlazeError` is Blaze's built-in structured error class. Throw it anywhere in your handler stack to produce a deterministic HTTP error response with a status code, a message, and an optional `meta` bag that the error handler can spread into the JSON body.

```ts theme={null}
import { BlazeError } from 'blaze'
```

**Constructor signature:**

<ParamField path="status" type="number" required>
  The HTTP status code to send in the response (e.g. `400`, `401`, `403`, `404`, `422`, `500`).
</ParamField>

<ParamField path="message" type="string" required>
  A human-readable error message included in the response body.
</ParamField>

<ParamField path="meta" type="Record<string, unknown>">
  Optional bag of additional fields spread into the JSON error response. Useful for machine-readable error codes, field-level validation errors, etc.
</ParamField>

```ts theme={null}
app.get('/admin/settings', async (req, res) => {
  // Throw BlazeError with a status, message, and structured meta
  if (!req.user) {
    throw new BlazeError(401, 'Authentication required')
  }

  if (!req.user.isAdmin) {
    throw new BlazeError(403, 'Forbidden', {
      code: 'INSUFFICIENT_PERMISSIONS',
      required: 'admin',
      current: 'user',
    })
  }

  const settings = await req.env.DB.prepare('SELECT * FROM settings').all()
  res.json(settings.results)
})
```

<Note>
  `BlazeError` extends `Error` and maintains a proper prototype chain, so
  `err instanceof BlazeError` works correctly in all environments — including
  after transpilation.
</Note>

***

## Global error handler

`app.onError()` registers a single global error boundary. Register it **after** all your routes and middleware so it catches errors from the entire app. The 4-argument signature — `(err, req, res, next)` — is what tells Blaze this is an error handler rather than a regular middleware.

```ts theme={null}
import { BlazeError } from 'blaze'

app.onError((err, req, res, next) => {
  // Distinguish BlazeError (structured) from unexpected errors
  if (err instanceof BlazeError) {
    // Log non-blocking via ctx.waitUntil so it doesn't delay the response
    req.ctx.waitUntil(
      logError({
        status: err.status,
        message: err.message,
        meta: err.meta,
        path: req.url,
        requestId: req.id,
        country: req.cf?.country,
      })
    )

    return res.status(err.status).json({
      error: err.message,
      ...err.meta,
    })
  }

  // Unexpected error — log at error level and return a generic 500
  req.ctx.waitUntil(
    logError({
      status: 500,
      message: err instanceof Error ? err.message : String(err),
      stack: err instanceof Error ? err.stack : undefined,
      path: req.url,
      requestId: req.id,
    })
  )

  res.status(500).json({ error: 'Internal Server Error' })
})
```

<Warning>
  Always register a global error handler in production. Without one, Blaze's
  default behaviour is to return a plain `{ error: 'Internal Server Error' }`
  with a 500 status — which leaks no details but also gives you no observability
  into what went wrong.
</Warning>

***

## Route-level error boundaries

Sub-routers can register their own error handlers with `router.onError()`. These catch errors thrown within that router's layer stack. You can handle specific error types locally and bubble the rest to the global handler by calling `next(err)`.

```ts theme={null}
const payments = app.Router()

payments.post('/charge', requireAuth, handleCharge)
payments.post('/refund', requireAuth, handleRefund)

// Only catches errors from within the payments router
payments.onError((err, req, res, next) => {
  // Handle payment-specific error codes locally
  if (err instanceof BlazeError && err.meta.code === 'CARD_DECLINED') {
    return res.status(402).json({
      error: 'Payment declined',
      code: 'CARD_DECLINED',
      hint: 'Check card details or try a different payment method.',
    })
  }

  if (err instanceof BlazeError && err.meta.code === 'INSUFFICIENT_FUNDS') {
    return res.status(402).json({
      error: 'Insufficient funds',
      code: 'INSUFFICIENT_FUNDS',
    })
  }

  // Anything else — bubble up to the app-level error handler
  next(err)
})

app.use('/payments', payments)

// Global handler catches everything that bubbled from payments.onError
app.onError((err, req, res, next) => {
  res.status(err instanceof BlazeError ? err.status : 500).json({
    error: err instanceof BlazeError ? err.message : 'Internal Server Error',
  })
})
```

***

## Async error propagation

Blaze wraps every handler call so that rejected Promises are automatically forwarded to `next(err)`. You do **not** need to write `try/catch` blocks in most handlers — Blaze catches uncaught async errors for you.

<CodeGroup>
  ```ts Without try/catch (recommended) theme={null}
  app.get('/users/:id', async (req, res) => {
    // If DB.prepare().first() rejects, Blaze forwards the error to next(err)
    const user = await req.env.DB
      .prepare('SELECT * FROM users WHERE id = ?')
      .bind(req.params.id)
      .first()

    if (!user) throw new BlazeError(404, 'User not found')
    res.json(user)
  })
  ```

  ```ts With explicit next(err) theme={null}
  app.get('/users/:id', async (req, res, next) => {
    try {
      const user = await req.env.DB
        .prepare('SELECT * FROM users WHERE id = ?')
        .bind(req.params.id)
        .first()

      if (!user) throw new BlazeError(404, 'User not found')
      res.json(user)
    } catch (err) {
      // Equivalent — but the try/catch is unnecessary for async handlers
      next(err)
    }
  })
  ```
</CodeGroup>

<Note>
  The only case where you need an explicit `next(err)` call is in
  **synchronous** middleware that conditionally forwards an error rather than
  throwing — for example, conditional auth logic that calls `next(err)` when
  validation fails and `next()` when it succeeds.
</Note>

***

## Error flow summary

<Steps>
  <Step title="Handler throws or calls next(err)">
    Any thrown error (sync or async) in a route or middleware is caught by Blaze's layer wrapper and forwarded to the routing engine as `next(err)`.
  </Step>

  <Step title="Normal layers are skipped">
    The router skips all remaining normal `(req, res, next)` handlers — they are not called while an error is in flight.
  </Step>

  <Step title="First matching error handler is called">
    The router finds the first Layer with a 4-argument function (`(err, req, res, next)`) downstream of where the error originated and invokes it.
  </Step>

  <Step title="Error handler calls next(err) to bubble">
    If the error handler can't handle a particular error type, it calls `next(err)` again to pass the error to the next downstream error handler — typically the global one on `app`.
  </Step>

  <Step title="Global handler responds">
    `app.onError()` is the last stop. If it calls `next(err)` or throws, Blaze returns a bare 500 response.
  </Step>
</Steps>
