Error handling & fallbacks
このコンテンツはまだ日本語訳がありません。
Three layers turn a thrown error into a response, innermost first:
a scoped handleErrors, then the server-wide withErrorHandler, then the
framework default (HttpError → its status + { error, … } JSON, anything
else → a generic 500). A fallback route answers requests that matched no
route at all.
Throwing an error
Section titled “Throwing an error”Any handler (or middleware) may throw an HttpError; reject is the
shorthand:
import { HttpError, Status, reject } from 'actor-ts/http';
get(async (req) => { const user = await findUser(req.params.id); if (!user) reject(Status.NotFound, 'no such user', { id: req.params.id }); return completeJson(Status.OK, user);});The default mapping turns that into 404 { "error": "no such user", "id": … }.
An HttpError can also carry response headers (e.g. WWW-Authenticate):
throw new HttpError(Status.Unauthorized, 'login required', undefined, { 'www-authenticate': 'Bearer realm="api"',});Scoped handling — handleErrors
Section titled “Scoped handling — handleErrors”handleErrors(handler, child) catches anything thrown inside child
(handlers and inner middleware) and sees the original error — before
any default mapping. Return a response to handle it, or null to decline
and let an outer handler (or the default) take over.
import { complete, completeJson, handleErrors, path, Status } from 'actor-ts/http';
const routes = handleErrors( (err) => err instanceof ValidationError ? completeJson(Status.BadRequest, { error: err.message, fields: err.fields }) : null, // decline → escalate path('api', apiRoutes),);Handlers nest outside-in like withMiddleware: the innermost handleErrors
gets first refusal. Placed around an auth middleware, it also catches that
middleware’s throws (e.g. a 401).
Unmatched requests — fallback
Section titled “Unmatched requests — fallback”fallback(handler) answers any request that matched no route — any method,
including unmatched OPTIONS/HEAD. It is wired to each backend’s
not-found hook, so it must sit at the root of the route tree (a
fallback under path() throws at compile time), and there is at most one
per server.
import { completeJson, concat, fallback, path, Status } from 'actor-ts/http';
const routes = concat( path('api', apiRoutes), fallback((req) => completeJson(Status.NotFound, { error: 'no such route', path: req.path })),);Server-wide — withErrorHandler
Section titled “Server-wide — withErrorHandler”The last resort for errors that escape every handleErrors, plus
backend-internal errors (body-parse failures). Set it on the server
builder:
const binding = await system.http(8080) .withErrorHandler((err, req) => { system.log.error(`[http] ${req.method} ${req.path}`, err); return completeJson(Status.InternalServerError, { error: 'internal error' }); }) .bind(routes);What the client sees, and what you keep
Section titled “What the client sees, and what you keep”The generic 500 at step 3 carries { "error": "Internal Server Error" } and
nothing else — deliberately. The thrown text routinely holds file paths, SQL
fragments, connection strings or a stack, and a 500 is by definition a case
nobody wrote a message for.
That redaction only works because the detail survives on the server. The
framework logs every throw that escapes handleErrors at error, and
passes the error value through, so a sink that formats stacks still gets
one:
[http] POST /api/orders → 500 after 12 ms [x-request-id=01J8Z…]A thrown HttpError is not in that log — it is a response the handler chose
(a 404, a 401), so it stays on the per-request debug line rather than
crying wolf. The same applies to a throw inside a fallback route, which is
mapped to a 500 without being re-thrown: without the log line it would leave
no trace anywhere.
The [x-request-id=…] suffix appears when the request carried that header
and its value is a well-formed id — see
Request id. Read it yourself with
requestIdOf(request) rather than by hand: it applies the same shape check,
which is what keeps a client-controlled string from forging a log record
through an embedded newline.
import { requestIdOf } from 'actor-ts/http';
const binding = await system.http(8080) .withErrorHandler((err, req) => { system.log.error(`[http] ${req.method} ${req.path} (${requestIdOf(req) ?? '-'})`, err); return completeJson(Status.InternalServerError, { error: 'internal error' }); }) .bind(routes);If you renamed the header via requestId({ headerName }), the framework’s
own line still reads the default one — log your id from withErrorHandler.
Precedence
Section titled “Precedence”| Order | Layer | Sees |
|---|---|---|
| 1 | innermost → outermost handleErrors | the original thrown value; may decline with null |
| 2 | withErrorHandler | whatever escaped step 1, plus backend-internal errors |
| 3 | framework default | HttpError → status + { error, …extra }; else generic 500 |
If a handler at step 1 or 2 throws, the next layer takes the new error.
Where to next
Section titled “Where to next”- HTTP overview — the bigger picture.
- HTML & XSS — build safe error pages.
- Security — the recommended middleware stack.
