Errors

Ingenium ships a small hierarchy of typed errors. Anything thrown in middleware or a handler bubbles to app.onError. If you don't register a handler - or you delegate by re-throwing - the default error boundary serializes IngeniumError subclasses with the right status code.

The error classes

import {
  IngeniumError,
  IngeniumNotFoundError,        // 404
  IngeniumUnauthorizedError,    // 401
  IngeniumMethodNotAllowedError,// 405 (auto-thrown on path match + method miss)
  IngeniumPayloadTooLargeError, // 413
  IngeniumValidationError,      // 422 with .fields
  IngeniumBadRequestError,      // 400
} from 'ingenium'

app.onError((err, ctx) => {
  if (err instanceof IngeniumValidationError) {
    return ctx.json({ error: err.message, fields: err.fields }, 422)
  }
  if (err instanceof IngeniumError) throw err  // delegate to default boundary
  ctx.json({ error: 'internal' }, 500)
})

The default boundary serializes any IngeniumError as { error, code, fields? } with the right status. Unknown errors become 500s. IngeniumMethodNotAllowedError writes the Allow response header automatically.

Production error classes

A handful of error classes are thrown by hardening features rather than your own handlers:

  • IngeniumTimeoutError (503) - handler exceeded requestTimeoutMs.
  • IngeniumHeaderInjectionError - ctx.set(name, value) rejected a value containing \r\n.
  • IngeniumUnserializableError (500) - ctx.json() was given a value with circular references or BigInt. safeJsonStringify(value) is exported for lenient mode.
  • IngeniumCsrfError (403, code CSRF_FAILED) - CSRF middleware rejected the request.

Re-throwing to delegate

Re-throwing from onError hands the error back to the default boundary, which is useful when you only want to customize a few cases:

app.onError((err, ctx) => {
  if (err instanceof IngeniumValidationError) {
    return ctx.json({ error: err.message, fields: err.fields }, 422)
  }
  throw err   // let the default boundary handle everything else
})

Where to next?