LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
Lesson 817 min read

Exception Filters & Error Handling

Handle errors cleanly in NestJS: built-in HTTP exceptions, the default error response, custom exceptions and writing your own exception filters.

On this page 11 sections

Introduction

Errors are a normal part of any API: a product does not exist, a user is not logged in, an email is already taken. Good APIs turn these into consistent, meaningful HTTP responses. NestJS makes this easy — you throw an exception anywhere, and Nest turns it into the right response.

What You Will Learn
  • How Nest's built-in exception layer works.
  • The built-in HTTP exception classes and when to use each.
  • How to create custom exceptions.
  • How to write an exception filter to control the error response.
  • How to apply filters to a route, a controller or the whole app.

The Built-in Exception Layer

Nest has a global exception filter that catches every unhandled exception. If the exception is an HttpException (or a subclass), Nest uses its status and message. Anything else — a TypeError, a database error — becomes a generic 500 response, so internal details never leak to clients:

Unknown error

Click Run to see what this code prints.

Built-in HTTP Exceptions

import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class ProductsService {
findOne(id: number) {
const product = this.products.find((p) => p.id === id);
if (!product) {
throw new NotFoundException(`Product ${id} not found`);
}
return product;
}
}
GET /products/99

Click Run to see what this code prints.

ExceptionStatusTypical Situation
BadRequestException400Input is malformed or breaks a business rule.
UnauthorizedException401Missing or invalid login credentials or token.
ForbiddenException403Logged in, but not allowed to do this.
NotFoundException404The requested resource does not exist.
ConflictException409Duplicate data, e.g. email already registered.
UnprocessableEntityException422Well-formed request that cannot be processed.
InternalServerErrorException500An unexpected server-side failure.

For any other status, throw the base class directly: throw new HttpException('Too many attempts', HttpStatus.TOO_MANY_REQUESTS).

Custom Exceptions

For errors that appear repeatedly in your domain, create your own exception by extending a built-in one:

import { ConflictException } from '@nestjs/common';
export class EmailTakenException extends ConflictException {
constructor(email: string) {
super(`An account with ${email} already exists`);
}
}
// usage in UsersService
if (await this.findByEmail(dto.email)) {
throw new EmailTakenException(dto.email);
}

Writing an Exception Filter

When you want full control over the error response — a different JSON shape, extra fields, logging — write an exception filter. @Catch() declares which exceptions it handles:

// src/common/filters/http-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
success: false,
statusCode: status,
message: exception.message,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}
GET /products/99

Click Run to see what this code prints.

Using @Catch() with no arguments catches every exception, not just HTTP ones — useful for a single "catch-all" filter that also logs unexpected errors.

Binding Filters

// 1. One route
@Get(':id')
@UseFilters(HttpExceptionFilter)
findOne() {}
// 2. A whole controller
@UseFilters(HttpExceptionFilter)
@Controller('products')
export class ProductsController {}
// 3. The entire app (main.ts)
app.useGlobalFilters(new HttpExceptionFilter());
Global Filters with DI

Filters registered with app.useGlobalFilters() cannot inject providers. To register a global filter that can, add { provide: APP_FILTER, useClass: HttpExceptionFilter } (APP_FILTER comes from @nestjs/core) to the providers of any module.

Common Mistakes

Avoid These Mistakes
  • Returning { error: 'not found' } with a 200 status instead of throwing NotFoundException.
  • Throwing plain Error objects for expected situations — clients get a vague 500 instead of a meaningful status.
  • Leaking stack traces or database messages to clients in a custom filter.
  • Catching exceptions in every controller with try/catch instead of letting filters handle them centrally.

Best Practices

  • Throw built-in HTTP exceptions from services for expected failures.
  • Create custom exception classes for errors repeated across your domain.
  • Use one global filter to keep the error response shape consistent across the API.
  • Log unexpected (non-HTTP) errors server-side, but return only a generic message to clients.

Frequently Asked Questions

It is the common, pragmatic choice in Nest apps. Very large apps sometimes throw domain-specific errors from services and translate them to HTTP in a filter, keeping services HTTP-agnostic.

Yes — pass an object to the exception constructor, e.g. new BadRequestException({ field: 'email', reason: 'invalid' }), and it becomes the response body.

Key Takeaways

  • Nest's built-in exception layer turns thrown exceptions into HTTP responses.
  • Built-in exceptions like NotFoundException map directly to status codes.
  • Non-HTTP errors become a safe, generic 500 response.
  • Exception filters (@Catch + ExceptionFilter) give full control over error responses.
  • Filters can be bound per route, per controller or globally.

Summary

Your API now fails gracefully with consistent, meaningful errors. Next, you will protect it — using guards and JSON Web Tokens to make sure only authenticated, authorized users can reach certain routes.

Next Lesson →

Guards & JWT Authentication