Exception handling is very important in front-end, including client-side and server-side exceptions. Previously, exception handling was to add err handling for each asynchronous function, which not only increased the workload, but also easily missed some exceptions. Fortunately, Angular6 provides ErrorHandler to handle exceptions (Ionic4 for IonicErrorHandler), the default ErrorHandler handling exceptions is to output it on the console, which obviously can not meet the demand, so you need to implement a GlobalErrorHandler.

Why use ErrorHandler to unify exception handling

  • Improve efficiency (for example, before for each asynchronous function need to pass err parameters to handle, now use ErrorHandler unified processing) The previous code for asynchronous exception handling is the following, just see so many err will feel dizzy, not to mention the logic code above, of course, for multiple asynchronous requests this is not the best way to handle, here only discuss exceptions.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
         (err: any) => {
                reject(err);
              }
            ), (err: any) => {
              reject(err);
            };
          }, (err: any) => {
            reject(err);
          });
        },
        (err: any) => {
          reject(err);
        }
      );
  • Exceptions that are not easily reproducible can be caught, especially problems that are not easily reproducible on the client side. If a client has a problem that cannot be reproduced locally and happens to forget to catch the exception, it would be very bad. Unified processing can input the exception information into the log and can quickly locate the problem.

  • It is also very bad to handle uncommon exceptions or runtime exceptions, and to prompt the user in a timely manner without crashing the program or exposing the exception directly to the user by not catching it.

  • For the user, a uniform, friendly exception message prompt can be displayed, creating a unique system style. For some exceptions, it is even possible to explain to the user why the exception is thrown and guide the user behavior to eliminate the exception. For example, if the user throws an insufficient balance exception when making a payment, the user should be prompted that the balance is insufficient, in addition to displaying a recharge portal (if the current system supports recharge) or prompting the user where to recharge.

Concrete implementation of catching exceptions with ErrorHandler

Step 1: Create ErrorService and LoggingService to get exception information and record exception logs

ErrorService: Get the exception information and stack of client side, exception information and status code of server side.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';

@Injectable()
export class ErrorService {

    getClientMessage(error: Error): string {
        if (!navigator.onLine) {
            return '暂无网络,请检查网络';
        }
        return error.message ? error.message : error.toString();
    }

    getClientStack(error: Error): string {
        return error.stack;
    }

    getServerMessage(error: HttpErrorResponse): string {
        return error.message;
    }

    getServerStatus(error: HttpErrorResponse): number {
        return error.status;
    }
}

LoggingService: Send exception information to the backend through Beacon API and record exception logs, including current logged-in user, exception occurrence time, exception information, etc.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Injectable } from '@angular/core';
import { DatePipe } from '@angular/common';
import { GlobalProvider } from '../globalprovider';
import { environment } from '@env/environment';
import { URL } from '../url';

@Injectable()
export class LoggingService {

    constructor(private global: GlobalProvider,
        private datePipe: DatePipe) { }

    logError(message: string, stack: string) {
        let currentUser = 'notLogin';
        if  (this.global.isLogin) {
            currentUser = this.global.currentUser.username;
        }
        const data = `{user: ${currentUser}, logtime: ${this.datePipe.transform(new Date(), 'yyyy-MM-dd HH:mm:ss')}, content: ${message}, stack: ${stack}}`;
        if (navigator.sendBeacon) {
            navigator.sendBeacon(environment.SERVER_URL + URL.log, data);
        } else {
            // Beacon is not supported
            const xhr = new XMLHttpRequest();
            xhr.open('post', environment.SERVER_URL + URL.log, false);
            xhr.send(data);
        }
    }
}

Step 2: Create GlobalErrorHandler

  • Create global-error-handler.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { ErrorHandler, Injectable, Injector } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {

    constructor(private injector: Injector) { }

    handleError(error: Error | HttpErrorResponse) {
    }
}
  • Add to app.module.ts
1
2
3
4
5
providers:  [
...
 { provide: ErrorHandler, useClass: GlobalErrorHandler},
...
]
  • Add exception handling logic
    • The current framework used is Ant Design, so the user prompts use NzMessageService.
    • When an exception occurs in the program, the handleError hook is automatically called, which can determine whether the exception is from the client or from the server.
    • In practical projects, it may be necessary to provide Services that display exception messages to satisfy the display of different exception messages.
    • For exception logging, it can be filtered according to the content of exceptions, and it is not necessary to log every exception.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { ErrorHandler, Injectable, Injector } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { LoggingService } from './services/logging.service';
import { ErrorService } from './services/error.service';
import { NzMessageService } from 'ng-zorro-antd';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {

    constructor(private injector: Injector) { }

    handleError(error: Error | HttpErrorResponse) {

        const errorService = this.injector.get(ErrorService);
        const logger = this.injector.get(LoggingService);
        const msg = this.injector.get(NzMessageService);

        let message;
        let status;
        let stackTrace;

        if (error instanceof HttpErrorResponse) {
            message = errorService.getServerMessage(error);
            status = errorService.getServerStatus(error);
           // Prompt for exception messages
            msg.error(message);
        } else {
            message = errorService.getClientMessage(error);
            stackTrace = errorService.getClientStack(error);
           // Prompt for exception messages
            msg.error(message);
        }
        // Record daily logs
        logger.logError(message, stackTrace);
    }
}

Note: Many people previously handled exceptions in the Interceptor, Interceptor can only handle HttpErrorResponse type of error, if handled here, then ErrorHandler will not capture. So in the Interceptor, encounter HttpErrorResponse error needs to be thrown.

1
2
3
4
5
6
7
catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          // Jump to login page or refresh token
        } else {
          return throwError(error);
        }
 })

Reference https://blog.dteam.top/posts/2020-02/angular%E6%88%96ionic-%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86errorhandler.html