Guides
Error handling
One error contract across every API route, and how the UI reads it.Every route handler in this template answers failures with the same shape:
A single shape means the client never has to guess how a given endpoint
reports trouble, and messages can be surfaced to users without a per-route
translation layer.
src/lib/errors.ts exports ApiError and the helpers around it. Throw
ApiError for anything the caller should see:
handleApiError returns the ApiError verbatim. Anything else becomes a
500 with a generic message, and the original is logged server-side — an
unexpected exception must never leak a stack trace or a query fragment to
the browser.
On the client, apiErrorMessage in src/lib/api-error-message.ts pulls the
message out of a parsed body, falling back to a readable default when the
body is missing or malformed. It also accepts the older { error: string }
shape, so a route that has not been migrated yet still produces something
readable:
src/app/error.tsx catches render-time errors for the whole app and
src/app/not-found.tsx handles unknown routes. Both are shell files, so
every composition has them.
When the Upstash variables are set, src/proxy.ts rate limits the mutating
routes and answers over-limit requests with the same error shape, code
rate_limited. When they are not set, the proxy passes requests through.
Callers cannot tell the difference from the contract's point of view, which
is what lets the same UI code run in both modes.
Last updated August 27, 2026Throwing
Reading
Rendering
Rate limiting
Edit this page on GitHub