How Exception Handling Works in NestJS: A Complete Guide to the Filter Architecture

NestJS implements a layered exception-filter system that intercepts errors thrown during request processing and translates them into HTTP responses through a four-step pipeline involving RouterProxy, ExceptionsHandler, and BaseExceptionFilter.

Exception handling in NestJS is built around a flexible, metadata-driven architecture that allows developers to catch and process errors at global, controller, or route levels. According to the nestjs/nest source code, the framework wraps every incoming request in a try/catch block and delegates errors to a chain of Exception Filters that determine the final response format.

The Exception Handling Pipeline

The framework processes uncaught exceptions through four distinct architectural layers defined in packages/core/exceptions/ and packages/core/router/.

Step 1: Request Routing and Error Capture

When a request arrives, the RouterProxy invokes the controller method inside a try/catch block. If an error bubbles up unhandled, the router immediately forwards it to the exception filter context.

In packages/core/router/router-exception-filters.ts, the RouterExceptionFilters.create method (lines 23-45) constructs an ExceptionsHandler instance for the current request. This handler serves as the central orchestrator for all subsequent error processing.

Step 2: Filter Resolution and Metadata

RouterExceptionFilters builds an ordered list of applicable filters by reading the EXCEPTION_FILTERS_METADATA key. This metadata is populated when you use the @UseFilters() decorator or register global filters.

The framework aggregates three tiers of filters:

  • Method-level filters (highest priority)
  • Controller-level filters
  • Global filters from ApplicationConfig.getGlobalFilters() and ApplicationConfig.getGlobalRequestFilters()

These are stored in an ExceptionsHandler instance during the creation phase (lines 31-44 of router-exception-filters.ts).

Step 3: Custom Filter Invocation

When an exception occurs, ExceptionsHandler.next() (lines 12-17 in packages/core/exceptions/exceptions-handler.ts) triggers invokeCustomFilters(). This method iterates through the filter list and selects the first match using selectExceptionFilterMetadata.

Located in packages/common/utils/select-exception-filter-metadata.util.ts (lines 4-13), this utility checks if the thrown error is an instance of any type defined in the filter's exceptionMetatypes array. A filter with an empty exceptionMetatypes array acts as a catch-all.

Step 4: Fallback to Base Filter

If no custom filter matches the exception type, control falls to BaseExceptionFilter.catch() in packages/core/exceptions/base-exception-filter.ts (lines 26-48).

This base implementation distinguishes between HttpException instances (which have predefined status codes and responses) and unknown errors. For unrecognized exceptions, handleUnknownError() (lines 50-71) generates a 500 Internal Server Error response and logs the stack trace through the injected HttpAdapterHost.

Key Architectural Components

Understanding the internal structure of NestJS exception handling requires familiarity with several core abstractions.

The ExceptionFilter Interface

Every custom filter must implement the ExceptionFilter interface defined in packages/common/interfaces/exceptions/exception-filter.interface.ts. The contract requires a single method:

catch(exception: unknown, host: ArgumentsHost): void

The ArgumentsHost parameter provides access to the execution context, allowing filters to retrieve the underlying request and response objects via host.switchToHttp().

Filter Context and Scoping

BaseExceptionFilterContext extends ContextCreator to instantiate concrete filter classes. It resolves scoped providers (request-scoped or transient) and extracts exceptionMetatypes via the reflectCatchExceptions utility. This ensures that filters can leverage the full dependency injection container, including services and repositories.

Global vs. Scoped Registration

Global filters apply to every route in the application and are retrieved through ApplicationConfig. Method and controller-level filters are discovered through reflection metadata and take precedence over global configurations. The framework merges these lists into a single execution chain during the request lifecycle.

Implementing Custom Exception Filters

The following examples demonstrate how to implement and register filters at different scopes.

Creating a Catch-All Filter

This filter intercepts every exception type and formats a consistent JSON response:

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    response.status(status).json({
      timestamp: new Date().toISOString(),
      path: request.url,
      error: exception instanceof Error ? exception.message : 'Unknown error',
    });
  }
}

Registering Global Filters

Apply a filter to every route in your application using useGlobalFilters():

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './all-exceptions.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new AllExceptionsFilter());
  await app.listen(3000);
}
bootstrap();

Controller and Route-Level Registration

Bind filters to specific controllers or methods using the @UseFilters() decorator:

import { Controller, Get, UseFilters } from '@nestjs/common';
import { AllExceptionsFilter } from './all-exceptions.filter';

@Controller('cats')
export class CatsController {
  @Get()
  @UseFilters(AllExceptionsFilter)
  findAll() {
    throw new Error('Something went wrong');
  }
}

When the above route throws, ExceptionsHandler.invokeCustomFilters selects AllExceptionsFilter because its empty exceptionMetatypes array matches all exception types.

Filter Selection Logic

The selectExceptionFilterMetadata utility determines which filter handles a specific error by evaluating the exceptionMetatypes array in the order filters were registered. The algorithm returns the first filter where:

  1. The exceptionMetatypes array is empty (catch-all), OR
  2. The thrown error is an instance of at least one type in the exceptionMetatypes array

This short-circuit evaluation means the order of filter registration matters significantly, with method-level filters evaluated before controller-level and global alternatives.

Summary

  • RouterExceptionFilters.create constructs an ExceptionsHandler for each request by aggregating metadata from EXCEPTION_FILTERS_METADATA.
  • ExceptionsHandler.next delegates to custom filters first via invokeCustomFilters, which uses selectExceptionFilterMetadata to find type matches.
  • BaseExceptionFilter serves as the final fallback, converting HttpException instances to proper HTTP responses and unknown errors to 500 status codes.
  • Custom filters implement the ExceptionFilter interface and access the execution context through ArgumentsHost.
  • Registration can occur globally via useGlobalFilters(), or locally using the @UseFilters() decorator with method-level priority.

Frequently Asked Questions

What order does NestJS use to evaluate exception filters?

NestJS evaluates filters in the order they were registered within each scope tier: method-level filters execute first, followed by controller-level filters, and finally global filters. Within each tier, the first filter whose exceptionMetatypes matches the thrown error type handles the exception, and subsequent filters are skipped.

How do I catch every exception in a NestJS application?

Create a filter with the @Catch() decorator without passing any exception classes, which leaves the exceptionMetatypes array empty. Register this filter globally using app.useGlobalFilters() in your bootstrap function to ensure it catches all unhandled errors across every route.

What happens when an error is not an HttpException?

If no custom filter catches the error, BaseExceptionFilter.handleUnknownError processes it. The method generates a 500 Internal Server Error response, logs the error stack trace through the HttpAdapterHost, and returns a generic error message to the client to prevent information leakage.

Can exception filters access dependency injection?

Yes. Exception filters are part of the NestJS instantiation lifecycle and can inject providers through their constructors. When using BaseExceptionFilterContext to create filter instances, the framework resolves all dependencies including scoped providers (request-scoped or transient) before the filter handles an exception.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →