Exception Filters
Exception filters provide centralized, type-safe error handling for HTTP routes. When a route handler (or a guard) throws, the filter chain catches the error and converts it to a response.
Interface
import type { ExceptionFilter, HttpExecutionContext } from '@onebun/core';
interface ExceptionFilter {
catch(
error: unknown,
context: HttpExecutionContext,
): OneBunResponse | Promise<OneBunResponse>;
}Creating Filters
Function-based filter
import { createExceptionFilter } from '@onebun/core';
import { OneBunBaseError } from '@onebun/requests';
const myFilter = createExceptionFilter((error, ctx) => {
if (error instanceof OneBunBaseError) {
return new Response(
JSON.stringify({ success: false, message: error.message, code: error.statusCode }),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
);
}
// Re-throw to let the next filter (or default) handle it
throw error;
});Class-based filter
import type { ExceptionFilter, HttpExecutionContext } from '@onebun/core';
class ValidationExceptionFilter implements ExceptionFilter {
catch(error: unknown, ctx: HttpExecutionContext): Response {
if (error instanceof ValidationError) {
return Response.json(
{ success: false, error: 'Validation failed', details: error.details },
{ status: 200 },
);
}
throw error; // pass to next filter
}
}HttpException
Throw HttpException from handlers, guards, or middleware to return a specific HTTP status code:
import { HttpException } from '@onebun/core';
// In a controller handler:
@Get('/:id')
async findOne(@Param('id') id: string): Promise<OneBunResponse> {
const item = await this.itemService.findById(id);
if (!item) {
throw new HttpException(404, 'Item not found');
}
return this.success(item);
}The default exception filter converts HttpException to a JSON response with the matching HTTP status:
| Input | Response |
|---|---|
throw new HttpException(400, 'Bad input') | HTTP 400 { success: false, error: "Bad input", statusCode: 400 } |
throw new HttpException(404, 'Not found') | HTTP 404 { success: false, error: "Not found", statusCode: 404 } |
throw new HttpException(409, 'Conflict') | HTTP 409 { success: false, error: "Conflict", statusCode: 409 } |
Note: Framework validation errors (
@Body(schema),@Param,@File) automatically throwHttpException(400, ...), so validation failures return HTTP 400 with a descriptive error message.
Applying Filters
Global (all routes)
import { OneBunApplication } from '@onebun/core';
import { myGlobalFilter } from './filters';
const app = new OneBunApplication(AppModule, {
filters: [myGlobalFilter],
});On a controller
import { Controller, UseFilters } from '@onebun/core';
@UseFilters(new ValidationExceptionFilter())
@Controller('/users')
class UserController extends BaseController { /* ... */ }On a single route
@Controller('/uploads')
class UploadController extends BaseController {
@UseFilters(createExceptionFilter((err, ctx) => {
if (err instanceof FileSizeError) {
return Response.json({ success: false, error: 'File too large' });
}
throw err;
}))
@Post('/')
async upload(@File() file: OneBunFile) { /* ... */ }
}Filter Priority
Filters are applied in priority order, from most specific to least specific:
Route-level filter → Controller-level filter → Global filter → Default filterEach filter may:
- Return a
Responseto short-circuit and send that response throw errorto pass the error to the next filter in the chain
The built-in default filter is always the final fallback and never throws.
Default Filter Behaviour
The defaultExceptionFilter is always active. It handles:
| Error type | Response body | Status |
|---|---|---|
HttpException | { success: false, error: message, statusCode: code } | exception's statusCode |
OneBunBaseError subclass | { success: false, error: message, statusCode: code } | 200 |
Any other Error or value | { success: false, error: 'Internal Server Error', statusCode: 500 } | 200 |
Note: HTTP 200 with a JSON error body is an intentional framework convention — it keeps all API responses structurally consistent (success / error both use the same envelope).
Accessing the Request in a Filter
const loggingFilter = createExceptionFilter((error, ctx) => {
const req = ctx.getRequest();
console.error(`Error on ${req.method} ${new URL(req.url).pathname}:`, error);
throw error; // let the next filter handle the response
});Async Filters
Filters can be asynchronous:
const auditFilter = createExceptionFilter(async (error, ctx) => {
await auditLog.record({
handler: ctx.getHandler(),
controller: ctx.getController(),
error: String(error),
});
throw error;
});Execution Order
Route Handler throws
→ Route-level filters (if any)
→ Controller-level filters (if any)
→ Global filters (if any)
→ Default filter (always present)
→ Response sent