HTTP errors
HTTPError and the JSON body a client receives.
A request that cannot be served answers with an HTTPError: a status code, a message, and an optional payload. Throw one from a handler - or return it - and it becomes the response:
import { defineHandler, notFound, query } from 'ohnejs';
export default defineHandler(async ({ params }) => {
const author = await query('Authors').where('UUID', params.id).findFirst();
if (!author) throw notFound('No such author');
return author;
});The client receives the error's status and a JSON body:
{ "statusCode": 404, "message": "No such author" }The shape is fixed: statusCode repeats the status, message carries the message, and data appears only when you attach one (below).
Named constructors
There is one constructor for each common status, and you import each from ohnejs:
badRequest() 400 Bad Request
unauthorized() 401 Unauthorized
forbidden() 403 Forbidden
notFound() 404 Not Found
conflict() 409 Conflict
payloadTooLarge() 413 Content Too Large
unsupportedMediaType() 415 Unsupported Media Type
unprocessable() 422 Unprocessable Content
tooManyRequests() 429 Too Many Requests
notImplemented() 501 Not ImplementedEach takes an optional message and optional data. If you omit the message, the status's standard reason phrase is used - notFound() answers with Not Found. For any other status, build the error directly:
import { HTTPError } from 'ohnejs';
throw new HTTPError(418, "I'm a teapot");The framework throws these itself when the request is wrong:
- A body over
api.maxBodySizeis a413. - A client past a route's rate limit gets a
429. - A JSON body reader given the wrong
Content-Typeis a415. - A malformed body is a
400.
Attaching data
The second argument is sent under data. It is machine-readable detail the client can act on:
throw unprocessable('Password too short', { field: 'password' });{ "statusCode": 422, "message": "Password too short", "data": { "field": "password" } }data is serialized into the body unchanged, so never put anything there that you would not show the client.
Unhandled errors
Any other throw inside a request, such as a bug, a rejected promise, or an error you did not map, becomes a generic response:
{ "statusCode": 500, "message": "Internal Server Error" }Nothing of the real error reaches the client: no message, no stack. The server logs it instead as an error block naming the route, and the stack shows only under DEBUG. A handler that throws never stops the server: the request is answered and the process keeps serving.
Once a streamed response has started, its status is already sent. A throw then cuts the body short instead. An HTTPError or a write failure ends it quietly, while any other throw is still logged.
To replace the generic response, such as with a branded error page, filter it with the error:response hook.
Write failures
Database writes turn their own failures into responses:
- A validation failure becomes a
422whosedata.errorsgives each failing field a message. When a create writes several locales and one fails,data.localenames it. - A busy database becomes a
503withRetry-After. - A delete blocked by a referencing record becomes a
409.
Translated messages
The default messages are message keys: notFound() reads api.http.notFound from the catalogs, so they resolve in the request's language. The field messages inside a write's 422 resolve the same way. A message you pass yourself is sent unchanged.