For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/concepts/errors.md.

Returned errors

In Dotpress, an error is a value: a handler or a middleware returns it, like any other result.

defineRoute<"/invoices/:id/pay", EmptyObject, false, unknown[]>(route: RouteConfig<"/invoices/:id/pay", EmptyObject, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.method?: HttpMethod | undefined

Defaults to get.

method
: 'post',
RouteConfig<"/invoices/:id/pay", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/invoices/:id/pay"
path
: '/invoices/:id/pay',
RouteConfig<"/invoices/:id/pay", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {
    id: string;
}, false, unknown>
handler
: async ({
req: RouteRequest<EmptyObject, {
    id: string;
}, false>
req
}) => {
const
const invoice: {
    id: string;
    status: "draft" | "paid";
} | undefined
invoice
= await
const invoices: {
    find(id: string): Promise<{
        id: string;
        status: "draft" | "paid";
    } | undefined>;
    markAsPaid(id: string): Promise<{
        id: string;
        status: "paid";
    }>;
}
invoices
.
function find(id: string): Promise<{
    id: string;
    status: "draft" | "paid";
} | undefined>
find
(
req: RouteRequest<EmptyObject, {
    id: string;
}, false>
req
.
DotpressRequest<{ id: string; }, QueryValues, unknown, UploadedFiles>.params: {
    id: string;
}
params
.
id: string
id
)
if (!
const invoice: {
    id: string;
    status: "draft" | "paid";
} | undefined
invoice
) return
function notFoundError(message?: string, data?: ErrorData): HttpError
notFoundError
('Invoice not found')
if (
const invoice: {
    id: string;
    status: "draft" | "paid";
}
invoice
.
status: "draft" | "paid"
status
=== 'paid') return
function conflictError(message?: string, data?: ErrorData): HttpError
conflictError
('Invoice already paid')
return
const invoices: {
    find(id: string): Promise<{
        id: string;
        status: "draft" | "paid";
    } | undefined>;
    markAsPaid(id: string): Promise<{
        id: string;
        status: "paid";
    }>;
}
invoices
.
function markAsPaid(id: string): Promise<{
    id: string;
    status: "paid";
}>
markAsPaid
(
const invoice: {
    id: string;
    status: "draft" | "paid";
}
invoice
.
id: string
id
)
}, })

The control flow stays visible: every way out of the handler is a return, readable from top to bottom. There is no exception to catch elsewhere, and no error class to declare.

One format for every error

Every error response has the same shape, whoever produced it: your handler, a middleware, the validation, the router or an unexpected exception.

{
  "status": 404,
  "error": "NOT_FOUND",
  "message": "Invoice not found"
}
FieldContent
statusThe HTTP status, also the status of the response
errorA stable code, for the clients to test
messageA message for humans
dataOptional details, such as the issues of a failed validation

Clients handle every error of the API with one piece of code.

The helpers

HelperStatusCode
badRequestError()400BAD_REQUEST
unauthorizedError()401UNAUTHORIZED
forbiddenError()403FORBIDDEN
notFoundError()404NOT_FOUND
conflictError()409CONFLICT
unprocessableError()422UNPROCESSABLE_ENTITY
tooManyRequestsError()429TOO_MANY_REQUESTS
internalError()500INTERNAL_ERROR

Each takes an optional message and optional data. For any other case, errorResponse() takes a status, a code, a message and data:

return errorResponse(422, 'INVALID_TRANSITION', 'A paid invoice cannot be cancelled', {
  status: 'paid',
})

Thrown errors work too

Returning is the convention, but a thrown HttpError gives the same response, from a handler, a middleware or a helper of the routes.

Services do not throw HttpError: they throw their own business errors, and the handler translates them into the HTTP error the client receives. See Errors of your services.

Unexpected errors

Anything else that is thrown, a bug or a database failure, is an unexpected error:

  • the client gets a 500 INTERNAL_ERROR, without details, so that nothing internal leaks;
  • the error is logged with the request ID, and passed to the onError hook of the app and of the plugins, to report it (Sentry, for instance).

In development, createApp({ exposeErrors: true }) adds the message and the stack of the error to the 500 responses. Never enable it in production.

See Errors for the errors produced by Dotpress itself (404, 405, 413…) and for error reporting, and the design decision for the reasons behind this choice.