Exception Filters in Depth

Catch everything with a global filter, preserve HttpException status codes, and return a consistent error contract.

A public API's error responses are part of its contract. Clients parse them. A single global exception filter guarantees every error - thrown by you, by a library, or by an unexpected bug - comes back in the same shape.

@Catch() scope

@Catch(HttpException) handles only that class; a bare @Catch() catches everything, including non-HTTP errors. A robust setup uses a catch-all filter that:

  1. Detects HttpException and reuses its status and message.
  2. Falls back to 500 for anything else, without leaking internals.
  3. Logs the original error with a correlation id.
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(err: unknown, host: ArgumentsHost) {
    const res = host.switchToHttp().getResponse();
    const status = err instanceof HttpException
      ? err.getStatus() : 500;
    res.status(status).json({ status, error: '...' });
  }
}

Preserve, don't flatten

When you catch an HttpException, pull its status and payload via getStatus() and getResponse() rather than hard-coding. A NotFoundException must stay a 404 - swallowing it into a generic 500 breaks clients and hides bugs.

Binding order and platform

Filters bound closer to the handler run first; the most specific matching filter wins. Register your catch-all globally with APP_FILTER so it can inject dependencies (a logger, a metrics client) - a filter registered with new in main.ts cannot.

Example

Example · typescript
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { Request, Response } from 'express';

@Catch() // catch-all: HttpException AND unexpected errors
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger('Exceptions');

  catch(exception: unknown, host: ArgumentsHost) {
    const http = host.switchToHttp();
    const res = http.getResponse<Response>();
    const req = http.getRequest<Request>();

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

    // Reuse the HttpException's own payload; hide internals otherwise.
    const detail = isHttp
      ? exception.getResponse()
      : 'Internal server error';

    const correlationId =
      (req.headers['x-request-id'] as string) ?? crypto.randomUUID();

    if (!isHttp) {
      this.logger.error(
        `[${correlationId}] ${String((exception as Error)?.stack ?? exception)}`,
      );
    }

    res.status(status).json({
      status,
      path: req.url,
      correlationId,
      timestamp: new Date().toISOString(),
      error: detail,
    });
  }
}

// DI-aware global registration:
// @Module({
//   providers: [{ provide: APP_FILTER, useClass: AllExceptionsFilter }],
// })
// export class AppModule {}

When to use it

  • A developer creates a global catch-all filter that catches unknown errors and returns a safe 500 response without leaking stack traces in production.
  • A team writes a TypeORM exception filter that detects unique-constraint violation errors and translates them into a user-friendly 409 Conflict response.
  • An engineer applies a domain-specific exception filter at the controller level while the global catch-all handles everything else across the application.

More examples

Global catch-all filter

Catches all exceptions (HttpException and unknown errors), logs them, and returns a sanitized JSON error with the correct HTTP status.

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

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger('ExceptionFilter');

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse();
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;
    this.logger.error(exception);
    res.status(status).json({
      statusCode: status,
      message: status === 500 ? 'Internal server error' : (exception as HttpException).message,
      timestamp: new Date().toISOString(),
    });
  }
}

TypeORM unique constraint filter

Catches TypeORM QueryFailedError and translates PostgreSQL unique constraint violations (code 23505) into a 409 Conflict response.

Example · ts
import { ExceptionFilter, Catch, ArgumentsHost, ConflictException } from '@nestjs/common';
import { QueryFailedError } from 'typeorm';

@Catch(QueryFailedError)
export class QueryFailedFilter implements ExceptionFilter {
  catch(exception: QueryFailedError, host: ArgumentsHost) {
    const res = host.switchToHttp().getResponse();
    const isUnique = (exception as any).code === '23505';  // PG unique violation
    if (isUnique) {
      res.status(409).json({ statusCode: 409, message: 'Resource already exists' });
    } else {
      res.status(500).json({ statusCode: 500, message: 'Database error' });
    }
  }
}

Scoped filter on one controller

Applies a payment-domain exception filter only to the PaymentsController while the global filter handles everything else.

Example · ts
import { Controller, Get, UseFilters } from '@nestjs/common';
import { PaymentExceptionFilter } from './payment-exception.filter';

@Controller('payments')
@UseFilters(PaymentExceptionFilter)  // only applies here
export class PaymentsController {
  @Get(':id/capture')
  capture(@Param('id') id: string) {
    return this.paymentsService.capture(id);  // may throw PaymentException
  }
}

Discussion

  • Be the first to comment on this lesson.