We use cookies to enhance your experience on the site
CodeWorlds

Exception Filters - managing crises

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.

What it is, and what it is not

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:

  • Middleware acts earliest, on the raw request, before NestJS knows where it is going.
  • Guard decides whether to admit - it returns
    true
    or
    false
    .
  • Pipe validates and transforms the input data.
  • Exception Filter steps in last, only when something went wrong, and decides what the client sees.

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.

@Catch - choosing what we handle

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".

Five steps of handling

The code above always performs the same sequence, and it is worth knowing as a whole:

  1. Intercept the exception in the
    catch()
    method.
  2. Retrieve the HTTP context with
    switchToHttp()
    .
  3. Read the status and data of the exception -
    exception.getStatus()
    .
  4. Format the JSON response - one uniform shape for the whole application.
  5. Send the response to the client with
    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.

HTTP codes - the language of refusal

The

statusCode
field carries the most important information, so choose it deliberately:

  • 400 Bad Request - the request is malformed, the client must fix it.
  • 401 Unauthorized - we do not know who you are; log in.
  • 403 Forbidden - we know who you are, and you may not.
  • 404 Not Found - the resource was not found.
  • 500 Internal Server Error - the server failed, not the client.

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.

Scope - where to post the clerk

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.

One filter, several exception types

@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.

Summary

The crisis office works and the reports share one form:

  • an Exception Filter is a class that intercepts exceptions and formats the error response - not Middleware, not a Guard, not a Pipe,
  • those three work on the road to the controller, the filter on the way back and only after an exception,
  • @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,
  • five steps: intercept, retrieve the context, read the status, format JSON, send,
  • never send the client a stack trace;
    timestamp
    and
    path
    in the response will save you time later,
  • 404 means "resource not found"; 4xx means "fix your request", 5xx means "this one is on us",
  • scope from narrowest:
    @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,
  • a filter translates internal exceptions into HTTP - your class names should not leave the building.

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.

Go to CodeWorlds