Exception Filters
Exception Filters
Section titled “Exception Filters”Exception filters give you a single place to translate any thrown error into a consistent HTTP response — without try/catch in every handler.
The problem they solve
Section titled “The problem they solve”Without filters, different plugins throw different error types with different shapes. Your handlers end up with scattered try/catch blocks producing inconsistent responses.
// Without filters — scattered handling@Get('/users/:id')async getUser(@Param('id') id: string) { try { return await db.findUser(id); } catch (err) { if (err instanceof DrizzleError) return c.json({ error: '...' }, 500); throw err; }}With exception filters, you define the translation once and every route benefits.
Creating a filter
Section titled “Creating a filter”Implement the ExceptionFilter<T> interface and decorate with @Catch:
import { ExceptionFilter, Catch } from 'veloce-ts';import type { Context } from 'hono';
@Catch(DrizzleError)class DatabaseExceptionFilter implements ExceptionFilter<DrizzleError> { catch(error: DrizzleError, c: Context): Response { return c.json( { error: 'Database error', code: error.code }, 500 ); }}@Catch accepts one or more error classes. The filter is applied when any instanceof check matches.
Registering globally
Section titled “Registering globally”const app = new Veloce();app.useFilter(new DatabaseExceptionFilter());app.useFilter(new ValidationExceptionFilter());await app.compile();Filters are checked in registration order. The first match wins — subsequent filters are skipped.
Multiple error types in one filter
Section titled “Multiple error types in one filter”@Catch(TypeError, RangeError)class JavaScriptErrorFilter implements ExceptionFilter { catch(error: Error, c: Context): Response { return c.json({ error: error.message }, 500); }}ZodError filter example
Section titled “ZodError filter example”import { ExceptionFilter, Catch } from 'veloce-ts';import { ZodError } from 'zod';import type { Context } from 'hono';
@Catch(ZodError)class ValidationExceptionFilter implements ExceptionFilter<ZodError> { catch(error: ZodError, c: Context): Response { return c.json( { error: 'Validation failed', details: error.issues.map(i => ({ field: i.path.join('.'), message: i.message, })), }, 422 ); }}Custom domain errors
Section titled “Custom domain errors”class UserNotFoundError extends Error { constructor(public readonly userId: string) { super(`User ${userId} not found`); }}
@Catch(UserNotFoundError)class UserNotFoundFilter implements ExceptionFilter<UserNotFoundError> { catch(error: UserNotFoundError, c: Context): Response { return c.json({ error: 'User not found', userId: error.userId }, 404); }}
// In your service — just throw, filter handles the restasync getUser(id: string) { const user = await db.findUser(id); if (!user) throw new UserNotFoundError(id); return user;}Filter execution order
Section titled “Filter execution order”Request → Route Handler (throws) → FilterManager → Filter 1: instanceof check → skip if no match → Filter 2: instanceof check → MATCH → return Response → (no match) → framework default error handlerFilterManager — advanced usage
Section titled “FilterManager — advanced usage”FilterManager is exported for integrations that need to trigger filters outside the router (e.g. WebSocket error handling):
import { FilterManager } from 'veloce-ts';
const manager = app.getFilterManager();const response = manager.handle(error, c);if (response) return response;Best practices
Section titled “Best practices”- One filter per error domain — keep filters small and focused.
- Register in specificity order — more specific error classes first; a catch-all
Errorfilter should be last. - Never swallow errors silently — always return a
Responseor re-throw.
// Specific filters firstapp.useFilter(new UserNotFoundFilter());app.useFilter(new DatabaseExceptionFilter());app.useFilter(new ValidationExceptionFilter());// Catch-all lastapp.useFilter(new GenericErrorFilter());