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.
- 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:
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; }}Click Run to see what this code prints.
| Exception | Status | Typical Situation |
|---|---|---|
| BadRequestException | 400 | Input is malformed or breaks a business rule. |
| UnauthorizedException | 401 | Missing or invalid login credentials or token. |
| ForbiddenException | 403 | Logged in, but not allowed to do this. |
| NotFoundException | 404 | The requested resource does not exist. |
| ConflictException | 409 | Duplicate data, e.g. email already registered. |
| UnprocessableEntityException | 422 | Well-formed request that cannot be processed. |
| InternalServerErrorException | 500 | An 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 UsersServiceif (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.tsimport { 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(), }); }}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());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
- 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.