Exception Handling Overview
NestJS ships with a built-in exceptions layer that catches any error thrown anywhere in your application and turns it into a well-formed HTTP response. You almost never have to write try/catch blocks in your controllers and services to send error responses — you simply throw, and the framework handles serialization, status codes, and the response body for you. Understanding how this layer maps thrown errors to JSON is the foundation for everything else in error handling: custom exceptions, exception filters, and a global catch-all.
The global exceptions filter
At the very end of the request lifecycle sits a default, framework-provided exception filter often called the global exceptions filter. When code inside a guard, pipe, interceptor, controller, or provider throws — and nothing downstream catches it — this filter intercepts the error before the response is sent. Its job is to inspect what was thrown and produce a sensible HTTP response, so that an uncaught error never leaks a stack trace or crashes the process.
Incoming request
-> Middleware
-> Guards
-> Interceptors (pre)
-> Pipes
-> Route handler <- throw happens here (or anywhere above)
-> Interceptors (post)
-> Exception filters <- global filter catches the thrown error
-> Response
Because this filter is always present, the safest way to signal a failure from anywhere in your code is simply to throw — no manual response handling required.
How thrown errors map to status codes and JSON
The base class for all HTTP-aware errors in Nest is HttpException, exported from @nestjs/common. It takes two core arguments: a response (a string or object that becomes the body) and a status code. The global filter reads both and shapes the outgoing response accordingly.
// cats.controller.ts
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
}
When the response argument is a string, Nest builds a JSON body for you containing the status code and the message. When it is an object, that object is serialized verbatim, giving you full control over the payload shape.
Output:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"statusCode": 403,
"message": "Forbidden"
}
To override the entire body, pass an object instead of a string:
throw new HttpException(
{
status: HttpStatus.FORBIDDEN,
error: 'This account is suspended',
},
HttpStatus.FORBIDDEN,
);
Output:
{
"status": 403,
"error": "This account is suspended"
}
Tip: Prefer the dedicated built-in exceptions like
NotFoundExceptionandBadRequestExceptionover rawHttpException— they set the correct status code for you and keep call sites readable.
Recognized versus unrecognized exceptions
The global filter treats two categories of errors very differently.
| Category | What it is | Resulting response |
|---|---|---|
| Recognized | Any instance of HttpException (or its subclasses) | The status code and body you specified |
| Unrecognized | Any other error — TypeError, Error, a thrown string, a DB driver error | A generic 500 Internal Server Error |
A recognized exception carries its own HTTP semantics, so Nest trusts it and forwards your status and message. An unrecognized exception is anything Nest can’t interpret as an HTTP error. To avoid leaking internal details, the filter masks it behind a generic 500 response and logs the original error to the server console.
// Recognized: maps to 404 with your message
throw new NotFoundException('Cat #42 not found');
// Unrecognized: a plain Error becomes a generic 500
throw new Error('Postgres connection refused');
Output for the unrecognized error:
HTTP/1.1 500 Internal Server Error
{
"statusCode": 500,
"message": "Internal server error"
}
This is a deliberate safety boundary. The client sees a clean, opaque 500 while the real cause — the database failure — is logged on the server. When you want richer error responses, the answer is not to expose raw errors but to throw a recognized HttpException (or wrap the failure in a custom one).
Anatomy of a recognized exception
Every HttpException exposes two helper methods used by the layer and by your own filters: getStatus() returns the numeric status code, and getResponse() returns the body (string or object). You can also pass options as a third argument to attach a cause for logging while keeping the public message clean.
import { HttpException, HttpStatus } from '@nestjs/common';
try {
await this.billingService.charge(userId);
} catch (err) {
throw new HttpException(
'Payment could not be processed',
HttpStatus.BAD_GATEWAY,
{ cause: err },
);
}
The cause is preserved internally for logging and debugging but is not serialized into the client response, so you keep the original stack without exposing it.
Best Practices
- Throw exceptions instead of returning error objects — let the exceptions layer own serialization and status codes.
- Use the specific built-in exception classes (
NotFoundException,ForbiddenException, etc.) rather than constructing rawHttpExceptioninstances by hand. - Never expose raw
Erroror driver errors to clients; wrap meaningful failures in anHttpExceptionso they become recognized. - Pass
{ cause }to retain the original error for logging without leaking internals to the response. - Keep status codes accurate (4xx for client mistakes, 5xx for server faults) so clients and monitoring behave correctly.
- Reach for a custom exception filter only when you need to reshape responses globally — the built-in layer covers the common cases.