Skip to content

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.

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.

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.

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.

@Catch(TypeError, RangeError)
class JavaScriptErrorFilter implements ExceptionFilter {
catch(error: Error, c: Context): Response {
return c.json({ error: error.message }, 500);
}
}
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
);
}
}
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 rest
async getUser(id: string) {
const user = await db.findUser(id);
if (!user) throw new UserNotFoundError(id);
return user;
}
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 handler

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;
  • One filter per error domain — keep filters small and focused.
  • Register in specificity order — more specific error classes first; a catch-all Error filter should be last.
  • Never swallow errors silently — always return a Response or re-throw.
// Specific filters first
app.useFilter(new UserNotFoundFilter());
app.useFilter(new DatabaseExceptionFilter());
app.useFilter(new ValidationExceptionFilter());
// Catch-all last
app.useFilter(new GenericErrorFilter());