title: 'Exception Filters · Nael Framework' description: 'Learn how to use exception filters for centralized error handling and custom error responses in Nael applications.'

Overview

Exception Filters

Exception filters let you control the exact flow of error handling and format error responses sent to the client. While middleware can catch errors, exception filters provide a more structured approach with access to the request context and the ability to chain multiple filters.

Creating an exception filter

Exception filters implement the ExceptionFilter interface, which requires a single catch() method. This method receives the exception and the request context, and must return a Response object.

http-exception.filter.ts
import { ExceptionFilter, HttpException } from '@nl-framework/http';import type { RequestContext } from '@nl-framework/http';export class HttpExceptionFilter implements ExceptionFilter {  async catch(exception: Error, context: RequestContext): Promise<Response> {    if (exception instanceof HttpException) {      return new Response(        JSON.stringify({          statusCode: exception.status,          message: exception.message,          timestamp: new Date().toISOString(),          path: new URL(context.request.url).pathname,        }),        {          status: exception.status,          headers: { 'Content-Type': 'application/json' },        }      );    }    // Handle other exceptions    return new Response(      JSON.stringify({        statusCode: 500,        message: 'Internal Server Error',        timestamp: new Date().toISOString(),      }),      {        status: 500,        headers: { 'Content-Type': 'application/json' },      }    );  }}

Registering exception filters

Exception filters are registered globally using the registerExceptionFilters() function, typically during application bootstrap:

main.ts
import { registerExceptionFilters } from '@nl-framework/http';// Register during application bootstrapregisterExceptionFilters(new HttpExceptionFilter());

Filters are executed in the order they are registered. If a filter successfully handles an exception (returns a Response), subsequent filters are not called. If a filter throws or re-throws the exception, the next filter in the chain is tried.

HTTP exceptions

The HttpException class provides a convenient way to throw HTTP errors with specific status codes. It includes static factory methods for common HTTP error statuses:

HTTP exception methods
import { HttpException } from '@nl-framework/http';// 400 Bad Requestthrow HttpException.badRequest('Invalid input');// 401 Unauthorizedthrow HttpException.unauthorized('Authentication required');// 403 Forbiddenthrow HttpException.forbidden('Access denied');// 404 Not Foundthrow HttpException.notFound('Resource not found');// 409 Conflictthrow HttpException.conflict('Resource already exists');// 422 Unprocessable Entitythrow HttpException.unprocessableEntity('Validation failed');// 500 Internal Server Errorthrow HttpException.internalServerError('Something went wrong');// 503 Service Unavailablethrow HttpException.serviceUnavailable('Service temporarily unavailable');// Custom status codethrow new HttpException(418, "I'm a teapot");

Use HttpException in your controllers and services to indicate HTTP-level errors:

posts.controller.ts
import { HttpException } from '@nl-framework/http';import { Controller, Get, Post, Body, Param } from '@nl-framework/http';@Controller('/posts')export class PostsController {  @Get('/:id')  async findOne(@Param('id') id: string) {    const post = await this.findPostById(id);    if (!post) {      throw HttpException.notFound(`Post with ID ${id} not found`);    }    return post;  }  @Post('/')  async create(@Body() data: CreatePostDto) {    if (!data.title || data.title.length < 3) {      throw HttpException.badRequest('Title must be at least 3 characters');    }    const existingPost = await this.findByTitle(data.title);    if (existingPost) {      throw HttpException.conflict('Post with this title already exists');    }    return await this.createPost(data);  }}

Validation exception filter

Create specialized filters for specific exception types. Here's a filter that handles validation errors:

validation-exception.filter.ts
import { ExceptionFilter } from '@nl-framework/http';import { ValidationException } from '@nl-framework/core';import type { RequestContext } from '@nl-framework/http';export class ValidationExceptionFilter implements ExceptionFilter {  async catch(exception: Error, context: RequestContext): Promise<Response> {    if (exception instanceof ValidationException) {      return new Response(        JSON.stringify({          statusCode: 400,          message: 'Validation failed',          errors: exception.issues.map((issue) => ({            field: issue.path.join('.'),            message: issue.message,          })),        }),        {          status: 400,          headers: { 'Content-Type': 'application/json' },        }      );    }    // Let other filters handle this    throw exception;  }}

Note that this filter re-throws the exception if it's not a ValidationException, allowing other filters in the chain to handle it.

RFC 7807 Problem Details

Implement standardized error responses following

RFC 7807

(Problem Details for HTTP APIs):

problem-json.filter.ts
import { ExceptionFilter, HttpException } from '@nl-framework/http';import type { RequestContext } from '@nl-framework/http';export class ProblemJsonFilter implements ExceptionFilter {  async catch(exception: Error, context: RequestContext): Promise<Response> {    const url = new URL(context.request.url);        if (exception instanceof HttpException) {      return new Response(        JSON.stringify({          type: `/errors/${exception.status}`,          title: this.getStatusText(exception.status),          status: exception.status,          detail: exception.message,          instance: url.pathname,        }),        {          status: exception.status,          headers: { 'Content-Type': 'application/problem+json' },        }      );    }    return new Response(      JSON.stringify({        type: '/errors/500',        title: 'Internal Server Error',        status: 500,        detail: exception.message,        instance: url.pathname,      }),      {        status: 500,        headers: { 'Content-Type': 'application/problem+json' },      }    );  }  private getStatusText(status: number): string {    const statusTexts: Record<number, string> = {      400: 'Bad Request',      401: 'Unauthorized',      403: 'Forbidden',      404: 'Not Found',      409: 'Conflict',      422: 'Unprocessable Entity',      500: 'Internal Server Error',      503: 'Service Unavailable',    };    return statusTexts[status] || 'Error';  }}

Logging exceptions

Exception filters can integrate with the logging system to track errors:

logging-exception.filter.ts
import { ExceptionFilter, HttpException } from '@nl-framework/http';import { Logger, LoggerFactory } from '@nl-framework/logger';import type { RequestContext } from '@nl-framework/http';export class LoggingExceptionFilter implements ExceptionFilter {  private readonly logger: Logger;  constructor(loggerFactory: LoggerFactory) {    this.logger = loggerFactory.create({ context: 'ExceptionFilter' });  }  async catch(exception: Error, context: RequestContext): Promise<Response> {    const url = new URL(context.request.url);    const status = exception instanceof HttpException ? exception.status : 500;    this.logger.error('Request exception', exception, {      method: context.request.method,      path: url.pathname,      status,    });    if (exception instanceof HttpException) {      return new Response(        JSON.stringify({          statusCode: exception.status,          message: exception.message,        }),        {          status: exception.status,          headers: { 'Content-Type': 'application/json' },        }      );    }    return new Response(      JSON.stringify({        statusCode: 500,        message: 'Internal Server Error',      }),      {        status: 500,        headers: { 'Content-Type': 'application/json' },      }    );  }}

Multiple filters

Register multiple exception filters to handle different types of errors. Filters are tried in order until one successfully handles the exception:

main.ts
import { registerExceptionFilters } from '@nl-framework/http';import { NaelFactory } from '@nl-framework/platform';import { LoggerFactory } from '@nl-framework/logger';const bootstrap = async () => {  const app = await NaelFactory.create(AppModule);  const loggerFactory = await app.get(LoggerFactory);  // Register filters in order  // First filter catches ValidationException  // Second filter catches HttpException  // Third filter catches everything else and logs  registerExceptionFilters(    new ValidationExceptionFilter(),    new HttpExceptionFilter(),    new LoggingExceptionFilter(loggerFactory),  );  await app.listen({ http: 3000 });};bootstrap().catch(console.error);

Tip: Register more specific filters first (like ValidationExceptionFilter), followed by more general filters. The last filter should be a catch-all that handles any unhandled exceptions.

Custom exceptions

Create custom exception types and corresponding filters for domain-specific errors:

database-exception.filter.ts
export class DatabaseException extends Error {  constructor(    message: string,    public readonly query?: string,    public readonly cause?: Error,  ) {    super(message);    this.name = 'DatabaseException';  }}export class DatabaseExceptionFilter implements ExceptionFilter {  async catch(exception: Error, context: RequestContext): Promise<Response> {    if (exception instanceof DatabaseException) {      console.error('Database error:', {        message: exception.message,        query: exception.query,        cause: exception.cause,      });      return new Response(        JSON.stringify({          statusCode: 500,          message: 'Database operation failed',        }),        {          status: 500,          headers: { 'Content-Type': 'application/json' },        }      );    }    // Let other filters handle this    throw exception;  }}

Catch-all filter

Always register a catch-all filter as the last filter to handle any unexpected errors:

catch-all.filter.ts
import { ExceptionFilter } from '@nl-framework/http';import type { RequestContext } from '@nl-framework/http';export class CatchAllFilter implements ExceptionFilter {  async catch(exception: Error, context: RequestContext): Promise<Response> {    // This catches everything that previous filters didn't handle    console.error('Unhandled exception:', exception);    // In production, don't expose internal error details    const isDevelopment = process.env.NODE_ENV === 'development';    return new Response(      JSON.stringify({        statusCode: 500,        message: 'Internal Server Error',        ...(isDevelopment && { error: exception.message, stack: exception.stack }),      }),      {        status: 500,        headers: { 'Content-Type': 'application/json' },      }    );  }}

Security Warning: Never expose internal error details (stack traces, database queries, etc.) in production environments. Only include detailed error information in development mode.

Filter execution order

Understanding how exception filters are executed is crucial for proper error handling:

  1. An exception is thrown in a controller, service, guard, or middleware
  2. The router catches the exception
  3. Exception filters are tried in registration order
  4. If a filter returns a Response, that response is sent to the client

  5. If a filter throws or re-throws, the next filter is tried
  6. If no filter handles the exception, a default 500 error response is returned

Best practices

  • Use specific exception types - Create custom exceptions for different error scenarios

  • Order matters - Register specific filters before general ones

  • Always have a catch-all - Ensure all exceptions are handled gracefully

  • Log errors - Track exceptions for debugging and monitoring

  • Don't leak sensitive data - Never expose internal details in production

  • Return consistent formats - Use a standard error response structure

  • Include request context - Add timestamps, request IDs, and paths to error responses

What's next?

Learn about Pipes for data transformation and validation, or explore Guards for authentication and authorization.