Error Handling
Every error response from a Blixis HTTP app — whether thrown by your code, a failed Zod validation, or an unhandled bug — has the same shape: RFC 9457 application/problem+json.
{ "type": "about:blank", "title": "Not Found", "status": 404, "detail": "Post 42 not found"}HttpException
Section titled “HttpException”Throw one from a controller method or a guard to produce a specific status:
import { HttpException } from "@blixis-io/http";
throw new HttpException(422, "Unprocessable", { field: "email" });status becomes the response status and the problem body’s status; detail becomes the body’s detail; the optional third argument is merged directly into the JSON body (used internally for Zod’s issues array on a 400). title is filled in automatically from the registered reason phrase for the status (422 is Unprocessable Entity, 429 is Too Many Requests, 503 is Service Unavailable, and so on for every 4xx and 5xx), falling back to "Error" for a status that has none.
An optional fourth argument carries response headers, for the statuses that need one:
throw new HttpException(429, "Try again later", undefined, { "retry-after": "30" });The response is always application/problem+json; a content-type in headers is ignored.
401 and WWW-Authenticate
Section titled “401 and WWW-Authenticate”A 401 is meant to tell the client how to authenticate (RFC 9110). UnauthorizedException takes the challenge as its second argument, and sends it as WWW-Authenticate:
throw new UnauthorizedException("Missing API key", 'ApiKey realm="admin"');Without one, no header is sent: the framework can’t know which scheme your app uses. The bearer-token guards in @blixis-io/auth send Bearer when no token was sent, and Bearer error="invalid_token" for a token that fails verification or the claims schema (RFC 6750). A 403 never carries a challenge.
Named exception classes
Section titled “Named exception classes”For the common cases, use the pre-built subclasses instead of HttpException directly:
| Class | Status | Default detail |
|---|---|---|
BadRequestException |
400 | "Bad Request" |
UnauthorizedException |
401 | "Unauthorized" |
ForbiddenException |
403 | "Forbidden" |
NotFoundException |
404 | "Not Found" |
ConflictException |
409 | "Conflict" |
PayloadTooLargeException |
413 | "Payload Too Large" |
UnsupportedMediaTypeException |
415 | "Unsupported Media Type" |
Every one takes an optional custom detail string:
throw new NotFoundException(`Post ${id} not found`);BadRequestException also accepts the same optional extra object as HttpException.
See Writing Custom Exceptions for subclassing HttpException for your own domain errors.
What happens automatically
Section titled “What happens automatically”You don’t have to throw these yourself for the framework’s own failure modes — they’re already wired in:
- A route that doesn’t exist →
404,"No route matches this path". - A route that exists, wrong method →
405, with anAllowheader listing the methods that are registered, anddetailnaming them too. - A guard denies (
canActivatereturnsfalse) →403 Forbidden. - A
@Body/@Query/@Paramschema fails validation →400,detail: "Validation failed", with anissuesarray (Zod’s own issue format) merged into the body. - Malformed JSON body →
400,"Invalid JSON body". - Wrong content-type on a body, or body over the size limit →
415/413(see Request Validation).
Uncaught errors → 500, and the message is hidden
Section titled “Uncaught errors → 500, and the message is hidden”Anything thrown that isn’t an HttpException — a real bug, a database connection failure, a @Returns schema mismatch, whatever — becomes a 500 with a generic detail: "An unexpected error occurred". The actual error, including its message and stack, is logged via console.error server-side, but never sent to the client. This is deliberate: an HttpException is an intentional, safe-to-show message; anything else might contain internal details you don’t want leaking into a response body — including a ResponseValidationError’s Zod issues, which would otherwise reveal your app’s internal response shape to whoever’s calling it. See Response Validation for why that one specifically is never an HttpException.
Return values that aren’t errors
Section titled “Return values that aren’t errors”A controller method’s return value becomes the response body too, via the same path — see Routing & Controllers for the full mapping (undefined → 204, a returned Response passed through unchanged, everything else → JSON with 200 or a status set via @HttpCode). Returning a raw Response is also how you redirect or set a non-JSON content type — see the Cookbook for both.
- A worked example of a custom exception hierarchy: Writing Custom Exceptions.
- Every exception class with full signatures:
@blixis-io/httpreference.