Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/elysiajs/documentation/llms.txt

Use this file to discover all available pages before exploring further.

Elysia provides a structured approach to error handling through the onError lifecycle hook, custom error classes, and the status helper. Understanding when to throw versus return a status, and how to define custom error types, makes your API responses predictable and fully type-safe.

Custom validation messages

When declaring a schema, attach an error string to any field to return a specific message when that field fails validation.
import { Elysia, t } from 'elysia'

new Elysia().get('/:id', ({ params: { id } }) => id, {
    params: t.Object({
        id: t.Number({
            error: 'id must be a number'
        })
    })
})
The raw string is returned as-is in the response body when validation fails.

Validation detail

To include the full validation context (field name, expected type, received value) alongside your custom message, wrap the error string with validationDetail.
import { Elysia, validationDetail, t } from 'elysia'

new Elysia().get('/:id', ({ params: { id } }) => id, {
    params: t.Object({
        id: t.Number({
            error: validationDetail('id must be a number')
        })
    })
})
To apply validationDetail globally without adding it to every field, handle it in onError:
import { Elysia, t } from 'elysia'

new Elysia()
    .onError(({ error, code }) => {
        if (code === 'VALIDATION') return error.detail(error.message)
    })
    .get('/:id', ({ params: { id } }) => id, {
        params: t.Object({
            id: t.Number({
                error: 'id must be a number'
            })
        })
    })
    .listen(3000)
By default, Elysia omits all validation details when NODE_ENV is production to prevent schema information leakage. Only the custom message field is preserved. Override this with allowUnsafeValidationDetails: true in the Elysia configuration.

Custom errors

Register custom error classes with .error() to get automatic code narrowing and auto-complete in onError.
import { Elysia } from 'elysia'

class MyError extends Error {
    constructor(public message: string) {
        super(message)
    }
}

new Elysia()
    .error({
        MyError
    })
    .onError(({ code, error }) => {
        switch (code) {
            case 'MyError':
                // `error` is typed as MyError here
                return error
        }
    })
    .get('/:id', () => {
        throw new MyError('Hello Error')
    })

Custom status code on error class

Add a status property to your error class so Elysia uses it automatically as the HTTP response status.
import { Elysia } from 'elysia'

class MyError extends Error {
    status = 418

    constructor(public message: string) {
        super(message)
    }
}
You can also set the status code explicitly in onError using the status helper:
import { Elysia } from 'elysia'

class MyError extends Error {
    constructor(public message: string) {
        super(message)
    }
}

new Elysia()
    .error({
        MyError
    })
    .onError(({ code, error, status }) => {
        switch (code) {
            case 'MyError':
                return status(418, error.message)
        }
    })
    .get('/:id', () => {
        throw new MyError('Hello Error')
    })

Custom response via toResponse

Define a toResponse() method on your error class to produce a fully custom Response object when the error is thrown.
import { Elysia } from 'elysia'

class MyError extends Error {
    status = 418

    constructor(public message: string) {
        super(message)
    }

    toResponse() {
        return Response.json({
            error: this.message,
            code: this.status
        }, {
            status: 418
        })
    }
}

Throw vs. return

The status helper can be either thrown or returned, and the behavior differs:
import { Elysia } from 'elysia'

new Elysia()
    .onError(({ code, error, path }) => {
        if (code === 418) return 'caught'
    })
    .get('/throw', ({ status }) => {
        // Caught by onError
        throw status(418)
    })
    .get('/return', ({ status }) => {
        // NOT caught by onError
        return status(418)
    })
UsageCaught by onErrorEden type inference
throw status(n)YesCorrect
return status(n)NoCorrect
Prefer return status(...) when you want to send an error response without triggering onError middleware. Use throw status(...) when you need centralized error handling to process the response.

Built-in error codes

Elysia narrows the code parameter in onError to built-in types automatically:
CodeDefault statusDescription
VALIDATION422Request failed schema validation
NOT_FOUND404No route matched the request path
PARSE400Request body could not be parsed
UNKNOWN500Unregistered or unrecognized error

Build docs developers (and LLMs) love