Photo by Yosuke Ota on Unsplash

On this page

This chapter follows Pipes and validation in NestJS.

Code anywhere in a Nest request, whether a guard, a pipe, a handler, or a service three calls deep, can stop the request by throwing. The exception travels up to the exceptions layer, where filters turn it into a response. Without any filter of your own, Nest’s built-in one already does something sensible. Custom filters exist for when you want a different error format, logging, or a translation from domain errors to HTTP status codes.

The built-in behavior

The default filter knows one class: HttpException. An instance of it, or of a subclass, becomes a response with its status code and message:

src/cats/cats.service.ts
throw new NotFoundException(`Cat ${id} not found`);
{
"message": "Cat 42 not found",
"error": "Not Found",
"statusCode": 404
}

Anything else, such as a TypeError or an error from the database driver, becomes a generic 500 with "message": "Internal server error". The original message is not sent, which keeps stack traces and SQL out of responses, and Nest logs the error to the console.

Built-in HTTP exceptions

@nestjs/common exports a subclass for each common status, so code reads by intent rather than by number:

Exception Status
BadRequestException 400
UnauthorizedException 401
ForbiddenException 403
NotFoundException 404
ConflictException 409
UnprocessableEntityException 422
InternalServerErrorException 500
ServiceUnavailableException 503

Each takes an optional message (or an object, which replaces the whole response body) and an options object with cause and description. The cause is not sent to the client, but it is kept for logging, so the lower-level error is not lost:

src/cats/cats.service.ts
try {
return await this.repository.insert(cat);
} catch (error) {
throw new ConflictException('A cat with this name already exists', { cause: error });
}

Custom exceptions

Domain code often should not know about HTTP. A plain error class keeps the service independent, and a filter maps it to a status later:

src/cats/cat-limit-reached.error.ts
export class CatLimitReachedError extends Error {
constructor(readonly ownerId: string, readonly limit: number) {
super(`Owner ${ownerId} already has ${limit} cats`);
}
}

When a status is part of the meaning anyway, extend HttpException or one of its subclasses instead. The default filter then handles it without any further code.

Writing a filter

A filter implements ExceptionFilter. @Catch() lists the exception types it handles, and catch() receives the exception and an ArgumentsHost, which gives access to the request and response.

src/cats/cat-limit-reached.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
import type { Response } from 'express';
import { CatLimitReachedError } from './cat-limit-reached.error';
@Catch(CatLimitReachedError)
export class CatLimitReachedFilter implements ExceptionFilter {
catch(exception: CatLimitReachedError, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();
response.status(HttpStatus.UNPROCESSABLE_ENTITY).json({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
error: 'CAT_LIMIT_REACHED',
message: exception.message,
limit: exception.limit,
});
}
}

Bind it with @UseFilters(CatLimitReachedFilter) on a controller or route, or globally. Filters resolve from the most specific level outward: route, then controller, then global. The first filter whose @Catch() matches handles the exception, and no other filter sees it.

A catch-all filter

@Catch() with no arguments catches everything. Registered globally, it gives every error in the app one JSON shape and one place to log. Using the HttpAdapterHost instead of the Express response keeps it working on Fastify too:

src/common/all-exceptions.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
constructor(private readonly adapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost) {
const { httpAdapter } = this.adapterHost;
const ctx = host.switchToHttp();
const status =
exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
if (status >= 500) this.logger.error(exception);
httpAdapter.reply(
ctx.getResponse(),
{
statusCode: status,
message: exception instanceof HttpException ? exception.message : 'Internal server error',
path: httpAdapter.getRequestUrl(ctx.getRequest()),
timestamp: new Date().toISOString(),
},
status,
);
}
}
src/app.module.ts
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [{ provide: APP_FILTER, useClass: AllExceptionsFilter }],
})
export class AppModule {}

Register specific filters such as CatLimitReachedFilter at the controller level, or as further global filters. When several global filters are registered, declare the catch-all first: Nest checks the most recently registered global filter first, so a catch-all registered last would shadow the specific ones. Keep the catch-all as the last resort, not as the place where every domain error is translated.

Next: Configuration in NestJS.

Check your understanding

Question 1 of 5

0 of 5 checked

  1. 1. What does the built-in filter send when a TypeError reaches it?

Search articles

Type a word to search published articles.

Open the search page