Global Exception Filter
A filter scoped to HttpException is great until something not an HttpException slips through — a thrown TypeError, a database driver rejection, or a plain string. Those land in Nest’s default handler and return a bare 500 with no context. A global catch-all filter closes that gap: it intercepts every error in the application, normalizes the response into one envelope, attaches a correlation ID, and logs the failure with enough detail to debug it in production. This page builds an AllExceptionsFilter, registers it both ways, and wires in correlation IDs and structured logging.
Catching everything with an empty @Catch()
The trick is the @Catch() decorator with no arguments. Where @Catch(HttpException) narrows to one type, an empty @Catch() matches any uncaught exception that reaches the exceptions layer. Because of that breadth your filter must inspect the exception and decide a sensible status: known HttpException instances expose getStatus() and getResponse(), while everything else collapses to a generic 500 Internal Server Error so you never leak stack traces to clients.
// all-exceptions.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
const correlationId =
(request.headers['x-correlation-id'] as string) ?? randomUUID();
const body = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
correlationId,
message,
};
this.logError(exception, body);
response.setHeader('x-correlation-id', correlationId);
response.status(status).json(body);
}
private logError(exception: unknown, body: Record<string, unknown>) {
const meta = `[${body.correlationId}] ${body.method} ${body.path}`;
if (exception instanceof HttpException && body.statusCode !== 500) {
this.logger.warn(`${meta} -> ${body.statusCode}`);
} else {
const stack = exception instanceof Error ? exception.stack : undefined;
this.logger.error(`${meta} -> ${body.statusCode}`, stack);
}
}
}
The filter never throws on a non-Error value — it falls back to a safe message. Client-facing errors (4xx) are logged at warn, while genuine server faults (5xx) are logged at error with the full stack trace so they surface in alerting.
Output:
[Nest] 14821 - 06/14/2026, 10:31:08 AM ERROR [AllExceptionsFilter] [3f0c2b9a-...] GET /orders/42 -> 500
TypeError: Cannot read properties of undefined (reading 'total')
at OrdersService.findOne (/app/dist/orders/orders.service.js:27:31)
...
And the response sent to the client:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
x-correlation-id: 3f0c2b9a-8d41-4f6a-9b2e-1c0f7d5a2e88
{
"statusCode": 500,
"timestamp": "2026-06-14T10:31:08.512Z",
"path": "/orders/42",
"method": "GET",
"correlationId": "3f0c2b9a-8d41-4f6a-9b2e-1c0f7d5a2e88",
"message": "Internal server error"
}
Why correlation IDs matter
A correlation ID is a single value that ties together a log line and the response a client received. When a user reports “request failed at 10:31,” they can hand you the correlationId from the response header, and you grep one string to find the exact stack trace. The filter reuses an inbound x-correlation-id header when present (so the ID flows across microservices) and generates a fresh UUID otherwise, then echoes it back on the response.
Registering the filter
There are two ways to register a global filter, and the choice depends on whether the filter needs dependency injection.
| Method | Where | Supports DI | Notes |
|---|---|---|---|
app.useGlobalFilters(new Filter()) | main.ts | No | Instance is created by you, outside any module |
APP_FILTER provider | a module | Yes | Nest instantiates it through the DI container |
Option A — useGlobalFilters in main.ts
Use this for a self-contained filter that needs nothing injected.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './all-exceptions.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
}
bootstrap();
Option B — APP_FILTER provider (DI-aware)
If the filter needs a config service, a custom logger, or an APM client, register it as a provider keyed by the APP_FILTER token. Nest then resolves its constructor dependencies normally.
// app.module.ts
import { 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 {}
Warning: A filter created with
new AllExceptionsFilter()lives outside the injector, so any@Inject()dependencies will beundefined. If your filter needs injected services, you must use theAPP_FILTERform instead.
Ordering: combine with narrower filters
Filters bind from narrowest scope to widest, and within a scope Nest applies them in reverse registration order. A common production setup keeps the catch-all as the safety net and registers a dedicated HttpExceptionFilter for richer handling of known errors. Because @Catch() matches everything, place it last conceptually — it only handles what no other filter claimed.
// app.module.ts (excerpt)
providers: [
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
],
Here the HttpExceptionFilter handles HttpException subclasses with its tailored envelope, and anything else falls through to AllExceptionsFilter.
Best Practices
- Use an empty
@Catch()so genuinely unexpected errors get a clean, standardized response instead of a raw500. - Never echo a non-
HttpException’s message to the client — collapse unknown errors to a genericInternal server errorto avoid leaking internals. - Generate or propagate a
correlationIdand return it as both a header and a body field so clients and logs line up. - Log
5xxfaults aterrorwith the full stack, and4xxclient errors atwarn— don’t drown alerts in expected validation failures. - Register via
APP_FILTERwhenever the filter needs DI; reserveuseGlobalFilters(new ...)for fully self-contained filters. - Keep the catch-all as a backstop and layer a typed
HttpExceptionFilterin front of it for known exception shapes.