Errors
Throw errors; never call next(err). The router forwards anything thrown by middleware or handlers to Express’s error path, where xil’s error handler renders one wire shape:
{ "code": "NOT_FOUND", "message": "Order 42 does not exist", "details": { "id": 42 }, "requestId": "…" }details is present only when the error carries some. The error’s class name is never included.
The HttpError family
Section titled “The HttpError family”import { NotFound, Conflict, HttpError, defineError } from "@architectine/xil";
throw new NotFound(); // 404 NOT_FOUND "Not Found"throw new NotFound("Order 42 does not exist", { id: 42 });throw new HttpError(418, "TEAPOT"); // any status and codeclass QuotaExceeded extends defineError(402, "QUOTA_EXCEEDED") {}| Class | Status | Code |
|---|---|---|
BadRequest |
400 | BAD_REQUEST |
Unauthorized |
401 | UNAUTHORIZED |
Forbidden |
403 | FORBIDDEN |
NotFound |
404 | NOT_FOUND |
MethodNotAllowed |
405 | METHOD_NOT_ALLOWED |
Conflict |
409 | CONFLICT |
Gone |
410 | GONE |
PayloadTooLarge |
413 | PAYLOAD_TOO_LARGE |
Unprocessable |
422 | VALIDATION |
TooManyRequests |
429 | RATE_LIMITED |
Internal |
500 | INTERNAL |
ServiceUnavailable |
503 | SERVICE_UNAVAILABLE |
Every constructor takes (message?, details?); the default message is the HTTP status text. isHttpError(err) narrows an unknown catch value to the HttpError type. A full list of the codes xil itself emits, across every module, is in the error code reference.
The error handler
Section titled “The error handler”createApp registers the handler for you and app.errors({ map, format, expose }) configures it. On a bare Express app, register it yourself last, after a 404 handler — see Using xil in an existing Express app.
Classification
Section titled “Classification”Errors are matched in this order:
-
map(err, ctx), if configured. Return a{ status, code, message, details? }object or anHttpErrorto decide the response, orundefinedto fall through. Use it to translate library errors (database constraint violations, upstream client errors) into your own codes —classifyErroris the function running this whole list, exported if you need the same classification outside the handler. Ifmapitself throws, that error is rendered as a 500. -
HttpError→ its status, code, message and details. -
Schema errors thrown directly by a validation library (an error with an
issuesarray, as Zod and Valibot throw fromparse) → 422VALIDATIONwith the issues indetails. -
body-parser errors → 400
BAD_REQUEST“Malformed request body”, or 413PAYLOAD_TOO_LARGEwith the limit and length indetails. -
Anything with a numeric
statusorstatusCodebetween 400 and 599 (thehttp-errorsconvention used across the Express ecosystem) → that status. The code is derived from the status text (NOT_FOUND,IM_A_TEAPOT). The error’s message is used only whenerr.exposeis true, as those libraries intend. -
Everything else → 500
INTERNAL. Withexpose: truethe body carries the real message and the stack; withexpose: falsethe message is “Internal Server Error”. The full error is always logged aterrorlevel, with the request id and route.
format
Section titled “format”format(mapped, ctx) replaces the envelope. It receives the classified { status, code, message, details? } and the context, and returns the body. The status is applied regardless.
errorHandler({ format: (mapped, ctx) => ({ error: { code: mapped.code, message: mapped.message }, traceId: ctx.requestId }),});The handler has no side-effect option. Alerting, metrics and audit logging go through onError hooks, which run exactly once per error.
When headers were already sent
Section titled “When headers were already sent”If the response was already partially written when the error occurred, the handler hands the error back to Express, which closes the connection. Nothing else can be done at that point.