Error Handling
Hectoday HTTP has two error paths: thrown Error objects go to onError, and thrown Response objects bypass everything and go directly to the client.
onError
Catches all thrown Error objects from onRequest and route handlers.
setup({
onError: ({ error, request, locals }) => {
const url = new URL(request.url);
console.error(`Error on ${request.method} ${url.pathname}:`, error.message);
return Response.json(
{ error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred" } },
{ status: 500 },
);
},
routes: [
/* ... */
],
}); Parameters
| Parameter | Type | Description |
|---|---|---|
error | Error | The thrown error |
request | Request | The original request |
locals | Partial<TLocals> | Partial because onRequest may not have completed |
Return value
Must return a Response. This response is passed through onResponse before being sent to the client.
Thrown Response objects
Throwing a Response object bypasses both onError and onResponse. The response goes directly to the client without modification.
onRequest: ({ request }) => {
const key = request.headers.get("x-api-key");
if (!key) {
throw Response.json(
{ error: { code: "UNAUTHORIZED", message: "API key required" } },
{ status: 401 },
);
}
return {};
}, Be aware that this skips onResponse entirely, so the response will not have any headers you normally add there (like X-Request-Id or X-Response-Time), and it will not be logged. Use this only when you need to bail out before onRequest finishes and you are fine with skipping the rest of the lifecycle.
Error Flow
Route handler or onRequest throws...
Error object Response object
│ │
▼ ▼
onError() Sent to client directly
│ (bypasses onError AND onResponse)
▼
onResponse()
│
▼
Sent to client Expected vs unexpected errors
onError is a safety net for unexpected errors: bugs, crashes, things you did not anticipate. It should not be used for control flow.
For expected outcomes like “not found,” “unauthorized,” or “forbidden,” return a Response directly from the handler instead of throwing:
resolve: (c) => {
if (!c.input.ok) {
return Response.json({ error: c.input.issues }, { status: 400 }); // expected: bad input
}
const { id } = c.input.params;
const book = findBook(id);
if (!book) {
return Response.json(
{ error: { code: "NOT_FOUND", message: "Book not found" } },
{ status: 404 },
); // expected: resource does not exist
}
return Response.json(book); // expected: success
}, Every outcome is a returned Response. Nothing is thrown. onError only runs if something truly unexpected happens, like findBook crashing due to a bug.