A legionary asks about a legion that does not exist. Instead of a readable refusal he gets a wall of text: the error class name, a stack trace, file paths from your server. Another endpoint returns a completely different shape in the same situation, because somebody handled the error by hand. The client does not know what to expect, and you have just shown the world the layout of your camp.
Rome had a separate office for crises. When something failed in a province, the report did not reach the Senate in the form it arrived from the field - a clerk rewrote it onto a uniform form. In NestJS that clerk is an Exception Filter.
An Exception Filter is a class that intercepts exceptions and formats the error response. That is its only job, and it is worth separating from its neighbours, because all four elements stand on a request's road:
true or false.The first three work on the road to the controller method. The filter works on the way back, and only when an exception fell on that road.
A filter begins with a decorator stating which exceptions it handles:
1@Catch(HttpException)
2export class LegionHttpExceptionFilter implements ExceptionFilter {
3 catch(exception: HttpException, host: ArgumentsHost) {
4 const ctx = host.switchToHttp();
5 const response = ctx.getResponse<Response>();
6 const request = ctx.getRequest<Request>();
7
8 const status = exception.getStatus();
9
10 response.status(status).json({
11 statusCode: status,
12 timestamp: new Date().toISOString(),
13 path: request.url,
14 message: exception.message,
15 });
16 }
17}@Catch(HttpException) narrows the filter to one exception type - this one handles only HTTP exceptions and lets everything else pass through. The bare decorator, @Catch(), would catch absolutely everything; that is useful for a last-resort filter, but then you lose the type information.The
catch method takes two arguments. The first is the exception itself. The second, host of type ArgumentsHost, is the more interesting one: NestJS serves not only HTTP but also WebSockets and microservices, so host holds the context in a protocol-independent form. host.switchToHttp() retrieves the HTTP context from it, giving access to the Request and Response objects. It switches no protocol - it merely says "treat this context as HTTP".The code above always performs the same sequence, and it is worth knowing as a whole:
catch() method.switchToHttp().exception.getStatus().response.status(...).json(...).Note what this response does not contain: no stack trace and no file names. That is deliberate - log the stack on the server, send the client only what helps them fix their request. The added
timestamp and path cost nothing and save an hour of searching when a user reports a problem.The
statusCode field carries the most important information, so choose it deliberately:The line between 4xx and 5xx matters here: fours say "fix your request", fives say "this one is on us". Returning 500 for bad input sends the client hunting for a fault that is not on their side.
The same filter can sit at three levels, from narrowest to widest:
1// 1. On a controller method - narrowest
2@Get(':id')
3@UseFilters(LegionHttpExceptionFilter)
4findOne(@Param('id') id: string) { }
5
6// 2. On the controller class - all its methods
7@Controller('legions')
8@UseFilters(LegionHttpExceptionFilter)
9export class LegionsController { }
10
11// 3. Globally - the whole application
12app.useGlobalFilters(new LegionHttpExceptionFilter());The rule is simple: the lower you place a filter, the narrower its scope. A filter on a method serves only that method, on a class - all the controller's methods, a global one - the entire application.
Remember this order, because it returns with guards, pipes and interceptors - in NestJS all four are applied the same way. In practice one global filter gives responses their common shape, while narrower filters add handling specific to a slice of the application.
@Catch accepts several types at once - useful when they come from one source and need similar handling:1@Catch(QueryFailedError, EntityNotFoundError)
2export class DatabaseExceptionFilter implements ExceptionFilter {
3 catch(exception: Error, host: ArgumentsHost) {
4 const response = host.switchToHttp().getResponse<Response>();
5
6 const status =
7 exception instanceof EntityNotFoundError
8 ? HttpStatus.NOT_FOUND
9 : HttpStatus.BAD_REQUEST;
10
11 response.status(status).json({
12 statusCode: status,
13 message:
14 status === HttpStatus.NOT_FOUND
15 ? 'Resource not found'
16 : 'Invalid database query',
17 });
18 }
19}Both exceptions come from TypeORM but mean different things, so inside
catch we tell them apart with instanceof. EntityNotFoundError is a 404 - the client asked for something that is not there. QueryFailedError is a 400, because it usually follows from data the client sent.And here you see the filter's second value beyond a uniform shape: it translates internal exceptions into the language of HTTP. The client need not know you use TypeORM - they get a code and a message that mean something to them. Hold that boundary, @name: the exception class names from your code should never leave the building.
The crisis office works and the reports share one form:
@Catch(ExceptionType) narrows the filter to that type; @Catch() with no argument catches everything,host.switchToHttp() retrieves the HTTP context from ArgumentsHost, giving access to Request and Response - it switches nothing,timestamp and path in the response will save you time later,@UseFilters on a method, @UseFilters on a class, app.useGlobalFilters() globally,@Catch accepts several types; inside you tell them apart with instanceof and map them to the right HTTP codes,In the next lesson you will learn to create your own exceptions, so there is something worth intercepting - with a readable hierarchy instead of a handful of
throw new Error. For now remember: a filter is the clerk who rewrites a report onto a uniform form - the client gets a code and a message, and the camp's layout stays in the camp.