Skip to main content

Overview

Elysia provides a robust error handling system with built-in error types, custom error handlers, and automatic error transformation for common HTTP errors.

Built-in error types

Elysia includes several built-in error classes defined in src/error.ts:120-160:

InternalServerError

import { InternalServerError } from 'elysia'

throw new InternalServerError('Something went wrong')
// Status: 500, Code: 'INTERNAL_SERVER_ERROR'

NotFoundError

import { NotFoundError } from 'elysia'

throw new NotFoundError('Resource not found')
// Status: 404, Code: 'NOT_FOUND'

ParseError

import { ParseError } from 'elysia'

throw new ParseError()
// Status: 400, Code: 'PARSE'

ValidationError

Automatically thrown when validation fails:
import { Elysia, t } from 'elysia'

const app = new Elysia()
  .post('/user', ({ body }) => body, {
    body: t.Object({
      username: t.String(),
      age: t.Number()
    })
  })
  .listen(3000)

// Invalid request automatically throws ValidationError
// Status: 422, Code: 'VALIDATION'

InvalidCookieSignature

import { InvalidCookieSignature } from 'elysia'

throw new InvalidCookieSignature('session')
// Status: 400, Code: 'INVALID_COOKIE_SIGNATURE'

Error handler

Use .onError() to handle errors globally or locally:

Basic error handling

import { Elysia } from 'elysia'

const app = new Elysia()
  .onError(({ code, error }) => {
    console.error('Error:', code, error.message)
    return {
      error: error.message,
      code
    }
  })
  .get('/error', () => {
    throw new Error('Something went wrong')
  })
  .listen(3000)

Handling specific error codes

import { Elysia } from 'elysia'

const app = new Elysia()
  .onError(({ code, error, set }) => {
    switch (code) {
      case 'VALIDATION':
        set.status = 422
        return {
          error: 'Validation failed',
          details: error.message
        }
      
      case 'NOT_FOUND':
        set.status = 404
        return {
          error: 'Route not found'
        }
      
      case 'INTERNAL_SERVER_ERROR':
        set.status = 500
        return {
          error: 'Internal server error'
        }
      
      default:
        return {
          error: 'Unknown error',
          message: error.message
        }
    }
  })
  .listen(3000)

Example from source

From example/error.ts:3-18:
import { Elysia, t } from 'elysia'

new Elysia()
  .post('/', ({ body }) => body, {
    body: t.Object({
      username: t.String(),
      password: t.String(),
      nested: t.Optional(
        t.Object({
          hi: t.String()
        })
      )
    }),
    error({ error }) {
      console.log(error)
    }
  })
  .listen(3000)

Local error handler

Define error handlers specific to individual routes:
const app = new Elysia()
  .get('/user/:id', ({ params, error }) => {
    const user = findUser(params.id)
    
    if (!user) {
      return error(404, 'User not found')
    }
    
    return user
  })
  .listen(3000)

ValidationError details

Access detailed validation errors:
import { Elysia, t } from 'elysia'

const app = new Elysia()
  .onError(({ code, error }) => {
    if (code === 'VALIDATION') {
      return {
        error: 'Validation failed',
        fields: error.all.map(e => ({
          field: e.path,
          message: e.summary,
          value: e.value
        }))
      }
    }
  })
  .post('/user', ({ body }) => body, {
    body: t.Object({
      username: t.String({ minLength: 3 }),
      email: t.String({ format: 'email' }),
      age: t.Number({ minimum: 18 })
    })
  })
  .listen(3000)

Custom error responses

Return custom responses from error handlers:
const app = new Elysia()
  .onError(({ code, error, set }) => {
    if (code === 'VALIDATION') {
      set.status = 422
      set.headers['x-error-type'] = 'validation'
      
      return new Response(
        JSON.stringify({
          success: false,
          error: error.message,
          timestamp: Date.now()
        }),
        {
          status: 422,
          headers: {
            'Content-Type': 'application/json'
          }
        }
      )
    }
  })
  .listen(3000)

Error inheritance

Error handlers cascade from global to local:
const app = new Elysia()
  // Global error handler
  .onError(({ code }) => {
    console.log('Global error handler:', code)
  })
  .group('/api', (app) => app
    // Group-level error handler
    .onError(({ code }) => {
      console.log('API error handler:', code)
    })
    .get('/user', () => {
      throw new Error('User error')
    }, {
      // Route-level error handler
      error({ error }) {
        console.log('Route error handler:', error.message)
        return { error: error.message }
      }
    })
  )
  .listen(3000)

Status helper

Return custom status codes easily:
import { Elysia, status } from 'elysia'

const app = new Elysia()
  .get('/created', () => 
    status(201, { message: 'Resource created' })
  )
  .get('/accepted', () => 
    status(202, { message: 'Request accepted' })
  )
  .get('/no-content', () => 
    status(204)
  )
  .listen(3000)

Custom error class

Create custom error types:
class AuthenticationError extends Error {
  code = 'AUTHENTICATION_ERROR'
  status = 401
  
  constructor(message: string = 'Authentication failed') {
    super(message)
  }
}

const app = new Elysia()
  .onError(({ code, error, set }) => {
    if (code === 'AUTHENTICATION_ERROR') {
      set.status = 401
      return {
        error: error.message,
        code: 'AUTHENTICATION_ERROR'
      }
    }
  })
  .get('/protected', ({ headers }) => {
    if (!headers.authorization) {
      throw new AuthenticationError('Missing authorization header')
    }
    return { data: 'Protected data' }
  })
  .listen(3000)

Production error handling

Different error responses for development vs production:
const isDevelopment = process.env.NODE_ENV === 'development'

const app = new Elysia()
  .onError(({ code, error, set }) => {
    if (isDevelopment) {
      // Detailed errors in development
      return {
        error: error.message,
        code,
        stack: error.stack,
        details: error
      }
    }
    
    // Generic errors in production
    switch (code) {
      case 'VALIDATION':
        set.status = 422
        return { error: 'Invalid request data' }
      
      case 'NOT_FOUND':
        set.status = 404
        return { error: 'Resource not found' }
      
      default:
        set.status = 500
        return { error: 'An error occurred' }
    }
  })
  .listen(3000)

Async error handling

Handle errors in async operations:
const app = new Elysia()
  .get('/user/:id', async ({ params, error }) => {
    try {
      const user = await fetchUser(params.id)
      return user
    } catch (err) {
      if (err.code === 'USER_NOT_FOUND') {
        return error(404, 'User not found')
      }
      throw err // Let global error handler deal with it
    }
  })
  .onError(({ error, set }) => {
    set.status = 500
    console.error('Unhandled error:', error)
    return { error: 'Internal server error' }
  })
  .listen(3000)

Error logging

Integrate error logging:
const app = new Elysia()
  .decorate('logger', {
    error: (message: string, error: Error) => {
      console.error(`[ERROR] ${message}`, {
        error: error.message,
        stack: error.stack,
        timestamp: new Date().toISOString()
      })
    }
  })
  .onError(({ code, error, logger, set }) => {
    logger.error(`Error occurred: ${code}`, error)
    
    set.status = 500
    return {
      error: 'An error occurred',
      requestId: generateRequestId()
    }
  })
  .listen(3000)

Complete error handling example

import { Elysia, t, NotFoundError, InternalServerError } from 'elysia'

const app = new Elysia()
  .state('users', new Map())
  
  // Global error handler
  .onError(({ code, error, set, path }) => {
    const timestamp = new Date().toISOString()
    
    console.error(`[${timestamp}] ${code} at ${path}:`, error.message)
    
    switch (code) {
      case 'VALIDATION':
        set.status = 422
        return {
          success: false,
          error: 'Validation failed',
          fields: error.all?.map(e => ({
            field: e.path,
            message: e.summary
          })),
          timestamp
        }
      
      case 'NOT_FOUND':
        set.status = 404
        return {
          success: false,
          error: error.message || 'Resource not found',
          timestamp
        }
      
      case 'PARSE':
        set.status = 400
        return {
          success: false,
          error: 'Invalid request body',
          timestamp
        }
      
      default:
        set.status = 500
        return {
          success: false,
          error: 'Internal server error',
          timestamp
        }
    }
  })
  
  .get('/users/:id', ({ params, store, error }) => {
    const user = store.users.get(params.id)
    
    if (!user) {
      throw new NotFoundError(`User ${params.id} not found`)
    }
    
    return { success: true, data: user }
  })
  
  .post('/users', ({ body, store }) => {
    const id = generateId()
    store.users.set(id, body)
    
    return {
      success: true,
      data: { id, ...body }
    }
  }, {
    body: t.Object({
      name: t.String({ minLength: 2 }),
      email: t.String({ format: 'email' }),
      age: t.Number({ minimum: 0, maximum: 150 })
    })
  })
  
  .delete('/users/:id', ({ params, store, error }) => {
    if (!store.users.has(params.id)) {
      throw new NotFoundError(`User ${params.id} not found`)
    }
    
    store.users.delete(params.id)
    return { success: true }
  })
  
  .listen(3000)
The error handler receives the full context, including set, store, and other context properties, allowing you to access application state during error handling.

Best practices

Throw specific error types (NotFoundError, ValidationError) rather than generic Error for better error handling.
Show detailed errors in development but sanitize error messages in production to avoid leaking sensitive information.
Always log errors with sufficient context (timestamp, path, user info) for debugging.
Use a consistent error response format across your API for easier client-side handling.
Provide clear, actionable feedback for validation errors to help users correct their input.
Be careful not to expose sensitive information (stack traces, internal paths, database errors) in production error responses.

Build docs developers (and LLMs) love