Chapter 11 — Exception Filters (Handling Errors)
Chapter 11 — Exception Filters (Handling Errors)
Hey everyone! Welcome back to Namaste NestJS! 🙏
Every chapter so far assumed things go right. Reality disagrees. Databases fail, IDs don't exist, tokens expire. This chapter is about the graceful failure — how Nest catches a thrown error and turns it into a clean, predictable HTTP response instead of a scary stack trace. Meet Exception Filters, and finish Season 2 strong.
What we will cover:
- How Nest handles exceptions out of the box
- The HttpException family (built-in errors)
- Throwing exceptions from your code
- Custom exceptions
- Writing a custom exception filter
- @Catch and the ArgumentsHost
- Applying filters (method, controller, global)
- Common mistakes
- Interview Questions
1. Nest's Built-in Exception Layer
Good news: Nest already has a global exception filter baked in. Any unhandled error becomes a JSON response automatically.
┌─────────────────────────────────────────────────────────────┐ │ THE BUILT-IN SAFETY NET │ ├─────────────────────────────────────────────────────────────┤ │ │ │ You throw an error ──→ Nest catches it ──→ JSON reply │ │ │ │ Known HttpException → uses its status + message │ │ Any other error → 500 Internal Server Error │ │ │ │ Your app never crashes on a thrown error. 🛡️ │ │ │ └─────────────────────────────────────────────────────────────┘
In simple words: you don't wrap every controller in try/catch. You throw, and Nest's exception layer converts it into the right HTTP response.
2. The HttpException Family
Nest ships ready-made exception classes for every common HTTP error. Throw one and the status code is set for you.
| Exception | Status | When to throw |
|---|---|---|
BadRequestException | 400 | Invalid input |
UnauthorizedException | 401 | Not logged in / bad token |
ForbiddenException | 403 | Logged in but not allowed |
NotFoundException | 404 | Resource doesn't exist |
ConflictException | 409 | Duplicate / state clash |
UnprocessableEntityException | 422 | Semantically invalid |
InternalServerErrorException | 500 | Unexpected server failure |
3. Throwing Exceptions From Your Code
Throw them anywhere — usually in a service when something can't be done:
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class CatsService {
private cats = [{ id: 1, name: 'Tom' }];
findOne(id: number) {
const cat = this.cats.find((c) => c.id === id);
if (!cat) {
throw new NotFoundException(`Cat #${id} not found`); // 👈 404, done
}
return cat;
}
}
HAND TRACE: GET /cats/999 (id 999 doesn't exist)
────────────────────────────────────────────────────
1. findOne(999) runs
2. cat = undefined → throw new NotFoundException('Cat #999 not found')
3. Nest's exception layer catches it
4. Reads status (404) + message
▼
Response: 404
{ "statusCode": 404, "message": "Cat #999 not found", "error": "Not Found" } ✔
You can customize the status and body:
// custom message + status via the base class:
throw new HttpException('Custom message', HttpStatus.I_AM_A_TEAPOT); // 418
// or pass an object as the response body:
throw new BadRequestException({ code: 'BAD_AGE', message: 'Age must be positive' });
4. Custom Exceptions
For domain-specific errors, extend HttpException (or a subclass) so you can throw meaningful, reusable errors:
import { HttpException, HttpStatus } from '@nestjs/common';
export class CatTooOldException extends HttpException {
constructor(age: number) {
super(`A cat can't be ${age} years old`, HttpStatus.BAD_REQUEST);
}
}
// usage:
if (age > 30) throw new CatTooOldException(age);
Pro tip: custom exceptions make business rules read like English and centralize their error messages, so you're not repeating string literals across the codebase.
5. Writing a Custom Exception Filter
When you want to control the exact shape of every error response (add a timestamp, a request path, a trace id), you write an exception filter. It's a class with @Catch() implementing ExceptionFilter.
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException) // 👈 catch only HttpExceptions (omit () to catch ALL)
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();
// build YOUR standard error envelope:
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}
THE RESULT — every error now looks like this:
{
"statusCode": 404,
"timestamp": "2026-07-06T10:00:00.000Z",
"path": "/cats/999",
"message": "Cat #999 not found"
}
Q: What is the ArgumentsHost?
A: It's a wrapper that gives the filter access to the underlying request/response — like the ExecutionContext for guards. host.switchToHttp() gets you the HTTP-specific request and response objects so you can craft the reply.
6. Applying Exception Filters
| Level | How |
|---|---|
| Method | @UseFilters(HttpExceptionFilter) on a handler |
| Controller | @UseFilters(HttpExceptionFilter) on the class |
| Global | app.useGlobalFilters(new HttpExceptionFilter()) or an APP_FILTER provider |
Note: a global filter that needs injected dependencies must be registered via an APP_FILTER provider (not new), so Nest's DI can build it — the same pattern you saw for global guards.
7. The Full Error Flow (Season 2 Recap)
Exception filters are the last stop in the pipeline. Here's how everything you learned in Season 2 fits together:
Request
│
Middleware → Guards → Interceptors(pre) → Pipes → HANDLER
│ │
│ (throws error?)
│ ▼
└──────────────────────────────────→ ★ EXCEPTION FILTER ★
turns it into a clean
HTTP JSON response ✔
That's the complete request pipeline. In Deep Dive 02 we'll trace a single request through every checkpoint, in exact order, including where errors short-circuit the flow.
8. Little Traps Beginners Fall Into
┌─────────────────────────────────────────────────────────────┐
│ COMMON MISTAKES ❌ │
├─────────────────────────────────────────────────────────────┤
│ │
│ Throwing a plain `new Error('...')` for client errors. │
│ → Becomes a generic 500. Use an HttpException subclass. │
│ │
│ Wrapping everything in try/catch "just in case". │
│ → Let Nest's exception layer do its job; throw instead.│
│ │
│ @Catch() with no argument catches EVERYTHING (incl. 500s).│
│ → Be intentional; @Catch(HttpException) is often safer.│
│ │
│ A global filter needing DI but registered with `new`. │
│ → Register via APP_FILTER so injection works. │
│ │
└─────────────────────────────────────────────────────────────┘
Interview Questions — Quick Fire!
Q: How does NestJS handle exceptions by default?
"Nest has a built-in global exceptions filter. When code throws, it catches the error and converts it to an HTTP response: if it's an HttpException it uses that exception's status and message, and anything else becomes a 500 Internal Server Error. This means the app doesn't crash on thrown errors and you rarely need manual try/catch in controllers."
Q: What is the HttpException family?
"It's a set of built-in exception classes representing common HTTP errors, like NotFoundException for 404, BadRequestException for 400, and UnauthorizedException for 401. You throw the appropriate one and Nest sets the correct status code and response body automatically."
Q: How do you create a custom exception?
"You extend HttpException and call super with a message and status code in the constructor. This lets you define domain-specific errors that read clearly and centralize their messages, then throw them anywhere in the app."
Q: What is an exception filter and when would you write one?
"An exception filter is a class decorated with @Catch implementing ExceptionFilter that takes full control over the error response. You write one when you need a consistent custom error shape across the API — for example adding a timestamp, request path, or trace id — or to handle specific exception types differently."
Q: What does the @Catch decorator control?
"It specifies which exception types the filter handles. @Catch(HttpException) catches only HttpExceptions, while @Catch with no arguments catches every unhandled exception, including unexpected ones. You choose based on whether you want to format a specific error type or act as a catch-all."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| Built-in filter | Nest auto-converts thrown errors to HTTP responses. |
| HttpException family | NotFoundException, BadRequestException, etc. set the status. |
| Throw, don't catch | Throw in services; let the exception layer respond. |
| Custom exception | extends HttpException for domain-specific errors. |
| Exception filter | @Catch + ExceptionFilter to shape the error response. |
| ArgumentsHost | Gives the filter the request/response objects. |
What's Next?
🎉 Season 2 complete — you can now follow a request through every checkpoint: middleware, guards, interceptors, pipes, the handler, and exception filters. Time to give your app a memory! Season 3 opens with Chapter 12: Databases with TypeORM — entities, repositories, and running real queries against a real database.
Keep coding, keep shipping! See you in the next one!
Post a Comment