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:
- Detects
HttpExceptionand reuses its status and message. - Falls back to
500for anything else, without leaking internals. - 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
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.
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.
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.
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