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.
@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 formnest g f http-exception # short alias → src/http-exception/http-exception.filter.tsnest g f http-exception --flat # no wrapping folder → src/http-exception.filter.tsnest g f errors/all-exceptions # nested path → src/errors/all-exceptions/all-exceptions.filter.tsnest g f errors/all-exceptions --flat # nested + flat → src/errors/all-exceptions.filter.tsnest g f http-exception --no-spec # skip the *.spec.ts test filenest 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:
Status
Class
400
BadRequestException
401
UnauthorizedException
402
PaymentRequiredException
403
ForbiddenException
404
NotFoundException
405
MethodNotAllowedException
406
NotAcceptableException
408
RequestTimeoutException
409
ConflictException
410
GoneException
412
PreconditionFailedException
413
PayloadTooLargeException
415
UnsupportedMediaTypeException
418
ImATeapotException
422
UnprocessableEntityException
500
InternalServerErrorException
501
NotImplementedException
502
BadGatewayException
503
ServiceUnavailableException
504
GatewayTimeoutException
505
HttpVersionNotSupportedException
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):
'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
Scope
How
Global
app.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:
Route-bound filter
Controller-bound filter
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.
Order matters when mixing catch-all with typed filters
If you bind a catch-all (@Catch()) and a typed filter (@Catch(HttpException)) at the same scope, declare the catch-all first in the @UseFilters(...) list. Otherwise the catch-all eats the exception before the typed filter gets a chance. Source: Catch everything.
Common recipes
Map a domain error to an HTTP status
Service code throws a domain-level error; the filter translates it into the right HTTP status without leaking implementation details to the controller.
// user-not-found.error.tsexport class UserNotFoundError extends Error { constructor(public readonly id: string) { super(`User ${id} not found`) }}
The controller stays clean: throw new UserNotFoundError(id). The filter owns the HTTP shape. Bind globally via APP_FILTER.
Platform-agnostic catch-all via HttpAdapterHost
Works under both @nestjs/platform-express and @nestjs/platform-fastify because it talks to the abstract HTTP adapter rather than Response.json() / Response.send() directly:
// app.module.tsimport { Module } from "@nestjs/common"import { APP_FILTER } from "@nestjs/core"import { AllExceptionsFilter } from "./all-exceptions.filter"@Module({ providers: [{ provide: APP_FILTER, useClass: AllExceptionsFilter }],})export class AppModule {}
HttpAdapterHost is only available after NestFactory.create() finishes wiring the adapter, which is why the official docs resolve it inside catch() rather than caching httpAdapter in the constructor. See HTTP adapter.
Extend BaseExceptionFilter to add logging without losing default behavior
When you only want to augment Nest’s built-in filter (e.g., always log unknown exceptions, then let Nest produce its standard 500 response), extend BaseExceptionFilter and call super.catch():
Register via APP_FILTER. Do not instantiate BaseExceptionFilter-extended filters with new at controller/method scope: the framework needs to inject the HttpAdapter reference for super.catch() to work.
Forward to Sentry, then re-respond
Capture the original error, attach context, then delegate to the default response shape:
// sentry.filter.tsimport { ArgumentsHost, Catch, HttpException } from "@nestjs/common"import { BaseExceptionFilter } from "@nestjs/core"import * as Sentry from "@sentry/node"@Catch()export class SentryFilter extends BaseExceptionFilter { catch(exception: unknown, host: ArgumentsHost): void { // Don't ship 4xx noise to Sentry; track 5xx and unknown errors. const isClientError = exception instanceof HttpException && exception.getStatus() < 500 if (!isClientError) { Sentry.captureException(exception) } super.catch(exception, host) }}
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.
useGlobalFilters() skips microservice/WebSocket gateways in hybrid apps
Same trap, same fix as the other lifecycle components. Use APP_FILTER or pass { inheritAppConfig: true } to connectMicroservice. Full explanation in Global providers > Hybrid apps gotcha.
Filter caught the exception → no further filter runs
Filters do not chain. Once a filter’s catch() returns (or sends the response), no other filter sees the exception. To compose behaviors (log + reshape), inherit from BaseExceptionFilter and call super.catch() instead of binding two separate filters.
@Catch() empty must come before typed filters at the same scope
When @UseFilters(CatchEverything, HttpExceptionFilter) lists the catch-all first, the typed filter still wins for HttpException because Nest tries each filter against the exception type. Reverse the order and the catch-all silently swallows everything. Declare the broadest filter first.
BaseExceptionFilter subclasses cannot be new'd at controller/route scope
They depend on the HttpAdapter injected by Nest. Use @UseFilters(MyFilter) (the class, not an instance) or register globally via APP_FILTER. Source: Inheritance.
Built-in HTTP exceptions are not logged by default
HttpException (and WsException, RpcException) extend IntrinsicException. Nest’s built-in filter treats them as part of normal flow and skips logging. If you want every error in the console, write a filter that logs and then delegates (super.catch() or your own response code).
Fastify uses response.send(), not response.json()
Under @nestjs/platform-fastify, the response shape is FastifyReply. Either swap .json(body) for .send(body) (and import FastifyReply from fastify), or use the HttpAdapterHost recipe which works on both adapters.
A try/catch inside the handler swallows the exception
Filters only see uncaught exceptions. If a controller wraps a service call in try/catch and converts the error into a response itself, no filter runs. That’s fine when the controller wants the local error shape; bring the throw back if you want the filter chain to handle it.
Common errors
Symptom
Likely cause
Every error becomes 500 Internal server error
The thrown error is not an HttpException and no custom filter handles it
Custom filter runs but the response is empty / hangs
Forgot 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