Skip to content
NestJS ns exceptions 4 min read

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 NotFoundException and BadRequestException over raw HttpException — 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.

CategoryWhat it isResulting response
RecognizedAny instance of HttpException (or its subclasses)The status code and body you specified
UnrecognizedAny other error — TypeError, Error, a thrown string, a DB driver errorA 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 raw HttpException instances by hand.
  • Never expose raw Error or driver errors to clients; wrap meaningful failures in an HttpException so 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.
Last updated June 14, 2026
Was this helpful?