Catch unhandled exceptions and turn them into HTTP responses. Think of filters as the last-chance handler: every other lifecycle component is a forward checkpoint that runs in order; a filter runs only when something blew up somewhere upstream (middleware, guards, interceptors, pipes, the handler, or the response interceptor chain).

Two things make filters unique: (1) bindings resolve bottom-up (route → controller → global, the opposite of every other layer), and (2) only the most specific filter wins — once it catches the exception, no other filter sees it. The most specific gets first dibs; the global filter is the safety net underneath.

What runs by default

Nest ships a built-in global filter that handles every uncaught exception. Behavior:

  • HttpException (and subclasses) → use the exception’s status and message.
  • Anything else → 500 Internal Server Error with body { "statusCode": 500, "message": "Internal server error" }.
  • Errors that look like http-errors (have statusCode + message) get those values respected instead of falling back to 500.

You only need to write a filter when you want to change the response shape, map domain errors to HTTP codes, log/forward the error somewhere (Sentry, DataDog), or handle non-HTTP transports.

Signature

import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common"
import { Request, Response } from "express"
 
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
  catch(exception: HttpException, host: ArgumentsHost): void {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()
    const status = exception.getStatus()
 
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.message,
    })
  }
}

@Catch(HttpException) registers the filter for HttpException and any subclass. Pass a comma-separated list (@Catch(HttpException, RpcException)) for several types, or no argument (@Catch()) for catch-all.

Generate with the CLI

nest generate filter http-exception      # full form
nest g f http-exception                  # short alias → src/http-exception/http-exception.filter.ts
nest g f http-exception --flat           # no wrapping folder → src/http-exception.filter.ts
nest g f errors/all-exceptions           # nested path → src/errors/all-exceptions/all-exceptions.filter.ts
nest g f errors/all-exceptions --flat    # nested + flat → src/errors/all-exceptions.filter.ts
nest g f http-exception --no-spec        # skip the *.spec.ts test file
nest g f http-exception --dry-run        # preview the file plan, write nothing

The schematic emits an empty @Catch() filter (catch-all) by default. Source: Nest CLI usages.

Built-in HTTP exceptions

All extend HttpException and live in @nestjs/common. Throw them anywhere and the default global filter responds with the right status:

StatusClass
400BadRequestException
401UnauthorizedException
402PaymentRequiredException
403ForbiddenException
404NotFoundException
405MethodNotAllowedException
406NotAcceptableException
408RequestTimeoutException
409ConflictException
410GoneException
412PreconditionFailedException
413PayloadTooLargeException
415UnsupportedMediaTypeException
418ImATeapotException
422UnprocessableEntityException
500InternalServerErrorException
501NotImplementedException
502BadGatewayException
503ServiceUnavailableException
504GatewayTimeoutException
505HttpVersionNotSupportedException

All accept (message?, options?) where options = { cause?, description? }. With a description:

throw new BadRequestException("Something bad happened", {
  cause: new Error("upstream timeout"),
  description: "Some error description",
})
// → { "message": "Something bad happened", "error": "Some error description", "statusCode": 400 }

cause is not serialized into the response; use it for log/Sentry context.

HttpException constructor

new HttpException(response: string | Record<string, any>, status: number, options?: HttpExceptionOptions)
  • string response → body is { statusCode, message: <string> }.
  • object response → that object replaces the body verbatim.
  • options.cause → preserved as Error.cause, useful for chained logging.

ArgumentsHost essentials

ArgumentsHost is the parent type of [[nestjs/fundamentals/guards|ExecutionContext]]. It does not expose getHandler() / getClass() (you don’t usually need handler metadata at the error layer):

MethodReturns
switchToHttp()HttpArgumentsHostgetRequest(), getResponse(), getNext()
switchToRpc()RPC context (microservices)
switchToWs()WebSocket context
getType()'http' | 'rpc' | 'ws' (or 'graphql' with @nestjs/graphql)
getArgs()Raw arguments tuple for the current handler

For platform-agnostic filters that work across both Express and Fastify, prefer the HttpAdapterHost recipe over reaching for Response directly.

Binding

ScopeHow
Globalapp.useGlobalFilters(new X()) or the [[nestjs/fundamentals/global-providers|APP_FILTER provider]]
Controller@UseFilters(X) or @UseFilters(new X()) on the class
Route@UseFilters(X) on the method

Pass the class, not an instance

@UseFilters(HttpExceptionFilter) is resolved by Nest’s DI container so the filter’s constructor and field injections are wired up. @UseFilters(new HttpExceptionFilter()) skips DI: any injected dependency is undefined. For a filter extending BaseExceptionFilter, the symptom is the “no http adapter” crash documented in common errors below — both applicationRef (constructor arg) and httpAdapterHost (@Optional() @Inject() field) end up undefined. Same trap covered in detail at Guards > Binding.

The global-scope variant of the same DI question — useGlobalFilters(new X()) vs APP_FILTER — has its own dedicated note: Global pipes, guards, interceptors, and filters via DI. It covers the side-by-side comparison, request-scope and hybrid-app implications, and when to reach for useClass vs useFactory.

import { Controller, Get, UseFilters } from "@nestjs/common"
import { HttpExceptionFilter } from "./http-exception.filter"
 
@UseFilters(HttpExceptionFilter)
@Controller("cats")
export class CatsController {
  @Get()
  list() {}
}

Order: route first, then controller, then global

Filters resolve bottom-up, the opposite of every other lifecycle component:

  1. Route-bound filter
  2. Controller-bound filter
  3. Global filter

Once a filter catches the exception, no other filter sees it. To layer behavior (e.g., always log, then format), use class inheritance from BaseExceptionFilter, not stacking. This is the opposite of pipes, guards, and interceptors, where every applicable instance runs.

When @Catch() is empty (catch-all)

@Catch()
export class CatchEverythingFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost): void { /* … */ }
}

Common recipes

Pairs with ValidationPipe

The ValidationPipe throws BadRequestException, which the default global filter renders as:

{
  "statusCode": 400,
  "message": ["email must be an email", "password must be longer than or equal to 8 characters"],
  "error": "Bad Request"
}

Override the response shape by passing exceptionFactory to ValidationPipe (preferred, keeps the filter chain simple) or by writing a BadRequestException filter that reformats the body. Stick with exceptionFactory unless you also want to reshape errors thrown elsewhere.

When to reach for it

  • Standardize the error response shape across the API ({ error, code, requestId, … }).
  • Map domain errors (UserNotFoundError, InsufficientStockError) to HTTP statuses without leaking them to controllers.
  • Forward errors to a tracker (Sentry, DataDog) before responding.
  • Add requestId / trace id to every error body for log correlation.

When not to

  • Validation/coercion: that’s a pipe’s job. Pipes throw, the filter renders.
  • Authorization decisions: a guard should reject before any handler work.
  • Wrapping the handler with timing/caching/retry: that’s an interceptor.
  • Mutating the raw request: middleware.

Gotchas

Common errors

SymptomLikely cause
Every error becomes 500 Internal server errorThe thrown error is not an HttpException and no custom filter handles it
Custom filter runs but the response is empty / hangsForgot response.status(...).json(...) (or .send(...) on Fastify)
Cannot read properties of undefined inside catch()Called host.switchToHttp() in a non-HTTP context. Branch on host.getType() first
BaseExceptionFilter-extended filter throws “no http adapter”Bound with new MyFilter() at method scope. Use the class form or APP_FILTER
Catch-all filter eats HttpException even though a typed filter is bound@UseFilters(...) listed the typed filter before the catch-all. Reverse the order
Filter doesn’t fire for a microservice or WebSocket handlerBound via useGlobalFilters() in a hybrid app. Switch to APP_FILTER
Validation errors come out as { statusCode, message, error } (array)That’s the default. Customize via ValidationPipe({ exceptionFactory }) in the validation recipe
Sentry sees thousands of 400s a dayCatch-all forwards every exception. Filter on HttpException && getStatus() < 500 first

See also