The exception handling middleware was mentioned in the middleware section above when we introduced Express. Express has its own exception middleware format (parameters). Nest has expanded its capabilities to make it more powerful and flexible.

abnormal

JS exceptions and their characteristics

Before we dive into Nest’s exception handling, let’s take a look at how JS handles exceptions. When an exception occurs in JS code, the program bubbles the exception information up the call stack layer by layer. This process is handled by a try… catch… The code is captured and processed, or passed to the JS engine.

main();
function main() {
  try {
    foo();
  } catch (err) {
    console.log(`An error occurred in foo.`);
  } finally {
    console.log(`Done! `); }}function foo() {
  console.log('inner foo');
  try {
    bar();
  } catch (err) {
    console.log(`An error occurred in bar.`);
  } finally {
    console.log(`finish in foo! `); }}function bar() {
  console.log('inner bar');
  throw Error('Error In bar');
}
Copy the code
PS C:\Users\django\source\nest-notes\projects\process_exception_filter\src> node .\exception.js
inner foo
inner bar
An error occurred in bar.
finish in foo!
Done!
Copy the code

If the try in foo… catch… Code comments, and the output would be:

inner foo
inner bar
An error occurred in foo.
Done!
Copy the code

If you remove all exception-catching code from your program, a large error message is printed, indicating which code threw the exception.

Nest standard exception

Nest builds an exception layer to handle exceptions that are not processed by a try… catch… Caught exceptions. That, at least, keeps the Nest application from shutting down in the event of an exception. For example, throw directly in Service

import { Injectable } from '@nestjs/common';

@Injectable(a)export class AppService {
  getHello(): string {
    throw 'abnormal'; }}Copy the code
curl -X GET "http://localhost:3000/"
{
  "statusCode": 500,
  "message": "Internal server error"
}
Copy the code

The built-in exception filter recognizes HttpException type as well as extension type errors. If the error type cannot be identified, the 500 error message above is returned.

HttpException

Nest’s exception base class, the constructor must pass in two objects:

  • Response defines the JSON response entity, with two attributes by default:
    • StatusCode indicates the HTTP statusCode.
    • Message a brief description of the exception;
  • Status Defines the HTTP status code, which is recommendednestjs/commonIn theHttpStatusEnumeration value of;
// /src/app.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';

@Injectable(a)export class AppService {
  getHello(): string {
    throw new HttpException('abnormal', HttpStatus.FORBIDDEN); }}Copy the code
curl -X GET "http://localhost:3000/"
# {"statusCode":403,"message":" exception "}
Copy the code

This is the standard exception throwing action, and Nest outputs the sequence of information to the request.

Built-in extended exception classes

Nest has the following exceptions (classes)

Exception (class) Wrong type Status code
BadRequestException Invalid request 400
UnauthorizedException unauthorized 401
NotFoundException Resource not found 404
ForbiddenException Disallows access to the specified resource 403
NotAcceptableException The content of the response that is not in the column in the Accept header 406
RequestTimeoutException The request timeout 408
ConflictException conflict 409
GoneException The requested resource is no longer available 410
HttpVersionNotSupportedException The HTTP version is not supported 505
PayloadTooLargeException The payload (data) of submitting the request is too large 413
UnsupportedMediaTypeException Unsupported media types 415
UnprocessableEntityException Unprocessable entity 422
InternalServerErrorException Internal server error (general error) 500
NotImplementedException Unsupported functions 501
ImATeapotException Teapot 418
MethodNotAllowedException Unacceptable request type in accept header 405
BadGatewayException The upstream service is abnormal 502
ServiceUnavailableException Service unavailable 503
GatewayTimeoutException Gateway corresponding timeout 504
PreconditionFailedException The premise given in the request header false 412

Feel free to view a built-in exception source code:

export class UnauthorizedException extends HttpException {
    constructor(objectOrError? :string | object | any,
    description = 'Unauthorized'.) {
    super( HttpException.createBody( objectOrError, description, HttpStatus.UNAUTHORIZED, ), HttpStatus.UNAUTHORIZED, ); }}Copy the code

An enumerated value of the description information and status code is passed to the parent class HttpException to obtain a specific subtype exception object.

Custom exception

Any application that is not very simple will have its own defined exception schema. Nest’s base exception class, HttpException, extends from the Error object, and only HttpException or exception objects inherited from it can be recognized and handled by Nest. In addition to the above default exception classes, developers can also define their own specialized exceptions. HttpException also provides a static method createBody for custom output information. Take a look at the source code:

  public static createBody(
    objectOrError: object | string, description? :string, statusCode? :number.) {
    if(! objectOrError) {return { statusCode, message: description };
    }
    return isObject(objectOrError) && !Array.isArray(objectOrError)
      ? objectOrError
      : { statusCode, message: objectOrError, error: description };
  }
Copy the code

An objectOrError can be a JSON object or a string.

  • If it is a string, the output message field is objectOrError, which corresponds to description, with statusCode
  • If it is JSON, it is entirely up to the developer to decide what information to output;
throw new HttpException({ Title: 'I am the King' }, HttpStatus.UNAUTHORIZED);
// {"Title":" I am king "}
// throw new HttpException(httpException.createBody ({Title: 'I am king'}), httpStatus.unauthorized); Same as above
throw new HttpException(HttpException.createBody("I am the king."."I was wrong"), HttpStatus.UNAUTHORIZED)
// {"message":" I am king ","error":" I am wrong "}
Copy the code

Httpexception. createBody The third status code argument is optional and will appear in the returned message if added (JSON), but is independent of the request HTTP status code. Such as:

throw new HttpException(HttpException.createBody("I am the king."."I was wrong",HttpStatus.TOO_MANY_REQUESTS), HttpStatus.UNAUTHORIZED)
Copy the code

The feedback is:

curl -X GET "http://localhost:3000/a"-I HTTP/1.1 401 Unauthorized X-powered-By: Express Content-Type: Application /json; charset=utf-8 Content-Length: 63 ETag: W/"3f-CkWpT4oA6UTSUc1stvBVr6qrvZo"
Date: Fri, 05 Feb 2021 03:45:06 GMT
Connection: keep-alive

{"statusCode": 429,"message":"I am the king."."error":"I was wrong"}
Copy the code

Abnormal filter

Exception filters allow developers to take over Nest’s work in the exception handling layer, giving them the freedom to return content and program flow.

Create a filter

nest g f unauthorized

import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';

@Catch(a)export class UnauthorizedFilter<T> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {}
}
Copy the code

ExceptionFilter is an interface with only one method: Catch. The method takes two arguments:

  • Exception object: an instance of an exception object thrown;
  • Service context object: contains request and Response; When entered into the filter, the response must have a message returned, or the whole process will stop and the requestor will not get a response and raise a timeout exception.

The @catch () decorator is used to tell Nest that the current class is an exception filter. You can take arguments of the exception type; if no arguments are set, all exceptions pass through this filter.

Completing the exception filter slightly would look like this:

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';
import { Response, Request } from 'express';

@Catch(HttpException)
export class UnauthorizedFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    // Get the context
    const ctx = host.switchToHttp();
    // Response control
    const response = ctx.getResponse<Response>();
    // Request object
    const request = ctx.getRequest<Request>();
    // Abnormal status code
    const status = exception.getStatus();
    response
    // Response status
      .status(status)
    // The response returns a call
      .send(
        ` [${ request.url }] \t  -The ${new Date().toLocaleDateString()} \t  The ${JSON.stringify(
          exception.getResponse(),
        )}`,); }}Copy the code

Execute the context ArgumentsHost

I mentioned earlier, but it’s important to reiterate Nest’s position: it’s strictly an IoC container, with no functionality of its own. A framework that is essentially implemented in the sense of a program architecture. At the service level (note, low and low), it can be an Express or Fastify HTTP service, a microservice or a WebSocket application. No matter what the high-level logic is, it is loosely coupled to the low-level. When low-level functionality needs to be invoked, objects and request parameters need to be retrieved through an Execution context. For example, guards, filters, and interrupts require some intervention and manipulation of the overall request-response process (such as prematurely terminating the response process).

In the Nest framework, two classes provide such an approach: ArgumentsHost and ExecutionContext. ExecutionContext is primarily used in guards and interrupts. Here we cover the former in detail.

The source code for Nest is as follows:

export interface ArgumentsHost {
  getArgs<T extends Array<any> = any[]>(): T;
  getArgByIndex<T = any>(index: number): T;
  switchToRpc(): RpcArgumentsHost;
  switchToHttp(): HttpArgumentsHost;
  switchToWs(): WsArgumentsHost;
  getType<TContext extends string = ContextType>(): TContext;
}
Copy the code

ContextType, RpcArgumentsHost, HttpArgumentsHost and WsArgumentsHost;

export type ContextType = 'http' | 'ws' | 'rpc';
export interface HttpArgumentsHost {
  getRequest<T = any>(): T;
  getResponse<T = any>(): T;
  getNext<T = any>(): T;
}
export interface WsArgumentsHost {
  getData<T = any>(): T;
  getClient<T = any>(): T;
}
export interface RpcArgumentsHost {
  getData<T = any>(): T;
  getContext<T = any>(): T;
}
Copy the code

From the source code, it is easy to infer that switchTo** is to get context objects (interfaces) and getArgs** is to get context variables. In fact, the two functions are almost identical. GetType is the type of the current context.

Get context

If you are writing an application that may be used in multiple service scenarios, then you need to fetch objects in different contexts, using the getType() method first to determine the context type:

if (host.getType() === 'http') {
  / / HTTP service
} else if (host.getType() === 'rpc') {
  // RPC (microservices)
} else if (host.getType() === 'ws') {
  // WebSocket
}
Copy the code

For example, in the exception filter catch code, the host variable of type ArgumentsHost is passed in. Then according to the above source code can be known:

  • host.switchToHttp()Corresponding HTTP service
  • host.switchToRpc()Corresponding microservices
  • host.switchToWs()Corresponding to the WebSocket service

When we get the context object (Host), we then get more detailed sub-instances, such as HTTP has:

  • getRequest()The request instance
  • getResponse()Response instance
  • getNext()Next hop function

In addition, you can use getArgs() to directly extract the key objects you need in one go

if (host.getType() === 'http') {const [req, res, next] = host.getArgs();
  /** equivalent to const req = host.getArgByIndex(0); const res = host.getArgByIndex(1); const next = host.getArgByIndex(2); * * /
} else if (host.getType() === 'rpc') {
  // RPC (microservices)
  const [root, args, context, info] = host.getArgs();
  // ...
} else if (host.getType() === 'ws') {
  // WebSocket
  const [data, client] = host.getArgs();
}
Copy the code

Filter binding

After writing the filter logic, you also need to specify where to apply the filter, for example:

  @Get('tea')
  @UseFilters(UnauthorizedFilter)
  teapot(): string {
    throw new ImATeapotException('I am a teapot');
  }
Copy the code

The @userFilters () decorator can accept single or multiple filter classes, or instances of filters. Passing parameters as classes is recommended because Nest relies on injection to manage instances uniformly and re-uses instances to save memory in scenarios where you need to use the same class of filters. Let’s look at the decorator source for the binding filter:

export const UseFilters = (. filters: (ExceptionFilter |Function) []) = >addExceptionFiltersMetadata(... filters);function addExceptionFiltersMetadata(. filters: (Function | ExceptionFilter)[]
) :MethodDecorator & ClassDecorator {
  return (
    target: any, key? :string| symbol, descriptor? : TypedPropertyDescriptor<any>,
  ) = > {
    const isFilterValid = <T extends Function | Record<string, any>>(
      filter: T,
    ) =>
      filter &&
      (isFunction(filter) || isFunction((filter as Record<string, any>).catch));

    if (descriptor) {
      validateEach(
        target.constructor,
        filters,
        isFilterValid,
        '@UseFilters',
        'filter',
      );
      extendArrayMetadata(
        EXCEPTION_FILTERS_METADATA,
        filters,
        descriptor.value,
      );
      return descriptor;
    }
    validateEach(target, filters, isFilterValid, '@UseFilters', 'filter');
    extendArrayMetadata(EXCEPTION_FILTERS_METADATA, filters, target);
    return target;
  };
}
Copy the code

Can be seen from the source code, a decorator to support multiple input filter, in turn, through addExceptionFiltersMetadata method validates the effectiveness of the input object (filters), and then stored into the method and the class metadata.

There are three ranges of binding, which are as follows:

  • A single method binding (the example above);
  • Controller bindings: Place decorators in annotations for controller classes
  • Global binding;

There are two ways to bind globally. One is to register filters with any Module as providers:

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { UnauthorizedFilter } from 'src/unauthorized.filter';
import { UserController } from './user.controller';

@Module({
  controllers: [UserController],
  providers: [{ provide: APP_FILTER, useClass: UnauthorizedFilter }],
})
export class UserModule {}
Copy the code

The other option is to set it in main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { UnauthorizedFilter } from './unauthorized.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new UnauthorizedFilter());
  await app.listen(3000);
}
bootstrap();
Copy the code

The difference is whether Nest manages dependencies, especially when filters (a type of Provider) require dependency injection. If Nest is needed to handle dependency injection, use the Module approach. And, in addition to specifying filters as classes, you can also register filters as providers. See the dynamic modules section for more information.

** Note: ** If you want to take over exceptions thrown by middleware, you must use global exception filtering, controller level filters do not apply to middleware.

Inheritance and architecture of exception filters

A well-defined application should also have well-defined exception classes. Different exception classes should have different handling procedures and logic. This is where Nest’s exception filter gets most interesting.

Combining the parameters of the @catch () decorator mentioned earlier (the exception type) with the scope of the filter, you can design a complex logical structure for handling exceptions. Suppose a system uses the following exception system:

Perform logical

When you register a filter, Nest generates a stack of exception filters: last add, first compare. As long as the exception type matches the @catch () setting, the filter logic is executed. It does not perform the same control transfer process as middleware. In the example above, if you register application A first and then register application exceptions, the Nest framework will perform application exceptions when an application type EXCEPTION occurs.

If you want the program to execute, the corresponding filter for the exception of the subclass first, allowing the parent class filter to be executed when no filter is set for one or some subclasses, the order of registration must be the parent class first, then the subclass.

Light logic filter

Often small projects dealing with filters have simpler requirements, such as one record and no other key logic. To leave basic exception handling to Nest, use the BaseExceptionFilter class:

import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch(a)export class AllexceptionFilter<T> extends BaseExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {
    console.log('some exceptions occurred');
    super.catch(exception, host); }}Copy the code

It is used in the same way as a normal filter and can be decorated with or without exception types. Super.catch () returns the current control to the Nest framework and the program continues.