DEV Community

loading...

Angular ErrorHandler - To Handle or Not to Handle?

buildmotion profile image Matt Vaughn ・14 min read

Please visit AngularArchitecture.com to sign-up for podcast notifications and other Angular resources.

This is what you get off-the-shelf. The ErrorHandler documentation on angular.io indicates that the default behavior is to print error messages to the console. This is fine for initial development. However, when the application goes to production, good luck trying to get access to the console of all the users of your application.

The application is running in the wild. Anything can happen and will. It is not a question if an error will happen, it more like when errors happen, right? Therefore, if the application is important to your business and users, you will want to know some valuable information about the error:

  • When and where do errors happen?
  • What kinds of error is it?
  • What is the origin of the error?
  • What information is contained in the error?
  • What does the user need to know, if anything?
  • What do we want to know about the error?
  • Where can I view error information about my application.

What is an Error?

An error indicates a problem that was not expected. The origin or source of the error may or may not be in your control.

Exceptions are a type of error that is expected or might be known to occur. Think about the scenario of a person attempting to retrieve cash from an ATM. If their balance is in the negative, the ATM will provide a message (exception) that there are no funds available. This article is mainly focused on errors from the application's perspective.

However, most web applications are going to make some HTTP calls. An HTTP call has the possibility of returning an error response. The reason for this most of the time fits into the category of a true error. The cause of the error response, in this case, is not related to the application's back end API or application.

Some application APIs will return an error status code as part of the API response. This provides some indication as to the type of error. However, since the origin is the actual application's API, the response will most likely return a well-known response in a specified schema or format that will the application to handle it accordingly. For example, if the application makes a call to the CreateAccount API, it might return an error status code and a list of messages that indicate the reason for the error/failure.

  • User name is already taken.
  • The password must contain a number and a special character.

Now, that we can expect an error condition from our application's API, we must be able to handle this specific type of error in the application.

External Errors/Exceptions

The Angular platform provides a mechanism to override and implement a custom ErrorHandler for your application. The default behavior of the default ErrorHandler is to write the messages to the browser console. Great for development and debugging. Not so good when the application is deployed to production.

/**
 * Provides a hook for centralized exception handling.
 *
 * The default implementation of `ErrorHandler` prints error messages to the `console`. To
 * intercept error handling, write a custom exception handler that replaces this default as
 * appropriate for your app.
 *
 * @usageNotes
 * ### Example
 *
 * 
 * class MyErrorHandler implements ErrorHandler {
 *   handleError(error) {
 *     // do something with the exception
 *   }
 * }
 *
 * @NgModule({
 *   providers: [{provide: ErrorHandler, useClass: MyErrorHandler}]
 * })
 * class MyModule {}
 * 
 */
export declare class ErrorHandler {
    handleError(error: any): void;
}
export declare function wrappedError(message: string, originalError: any): Error;

Angular Error Handling, Logging, and Notification

Here are some things to consider when implementing an Error Handling strategy for your application.

Error Handling

  • Determine where error handling should take place in the application - responsibility?
  • Single source of error handling?
  • What do you do with the error details and source?
  • Do you deliver a generic error message, "Oops!"?
  • How do you handle different types of errors?
    • HttpClient use Observables
    • Application
    • 3rd-party library
    • API/Server

Error Notification

  • Determine if the end-user should be notified of the error.
  • Are there any specific messages that need to be shown to the user?
  • Should application/system administrators be notified - how?

Error Logging (Tracking)

  • Determine what is required for logging/tracking.
  • Need to understand the context of the error.
  • Do not log too little, you require relevant and contextual information.
  • When did it occur? Where? Who? What?

Custom Error Classes

  • instanceOf
  • extending Error Classes
  • adding rich meta data

Error Sources

We can categorize error sources in (3) groups.

  1. External
  2. Internal
  3. Application

External Errors

External errors are external from the running application. In our case, they are external to our Angular application running in a client browser. These occur on servers or APIs outside of our application's runtime environment. Server errors happen while attempting to process the request or during processing on the server.

  • database connection errors
  • database errors
  • application exceptions
  • application not available

Server

Most Angular applications use some kind of back end API(s) or server to perform additional application processing. Even if the Angular application is serverless - meaning that it doesn't have its specific server associated with the application, the application may use several APIs and functions that are hosted by other providers (think: APIs for MailChimp, Contentful, Firebase, Medium, etc.).

Regardless of the source of these external errors, an Angular application will need to handle them gracefully.

  • 500 Errors

    The server failed to fulfill a request.

    Response status codes beginning with the digit "5" indicate cases in which the server
    is aware that it has encountered an error or is otherwise incapable of performing the
    request. Except when responding to a HEAD request, the server should include an entity
    containing an explanation of the error situation, and indicate whether it is a
    temporary or permanent condition. Likewise, user agents should display any included
    entity to the user. These response codes apply to any request method.

Here is an example of some of the types of 500 Server Errors that can happen.

  • 500 Internal Server Error > A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.[62]
  • 501 Not Implemented > The server either does not recognize the request method, or it cannot fulfill the request. Usually, this implies future availability (e.g., a new feature of a web-service API).[63]
  • 502 Bad Gateway > The server was acting as a gateway or proxy and received an invalid response from the upstream server.[64]
  • 503 Service Unavailable > The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state.[65]

Internal Errors

An internal error origin is from the application's API. These types of error, as mentioned previously, will most like use a specific HTTP error status code. However, it will also include a detailed response in a known format to allow the consumer of the API to handle the response. Even though the HTTP status code is an error code, the application should

  • Security and/or a permission issue.
  • Business rule violation(s).
  • Data validation exception(s).

Server

  • 400 Errors

    This class of status code is intended for situations in which the error seems to have been caused by the client. Except when responding to a HEAD 
    request, the server should include an entity containing an explanation of the error situation, and whether it is a temporary or permanent condition. These status codes apply to any request method. User agents should display any included entity to the user.
    

Client (Browser) - JavaScript

JavaScript has an Error object that all errors in JavaScript derive from. The standard available properties for an error are as follows:

  • columnNumber
  • fileName
  • lineNumber
  • message
  • name
  • stack

This is the information that we see in the Console of the browser's developer tools. These types of errors are usually unexpected

Here is a list of specialized types of errors that can occur.

Application Errors

Applications can also be the source of errors. These types of errors cause the current application flow to redirect to a registered provider for Handling the error. Developers, coders, and software engineers will not write perfect code. There are inputs, outputs, processing of information, algorithms, calculations, and other things that happen during the runtime of an application that it is impossible to anticipate all things.

Therefore, errors happen and we will see them in the following cases:

  1. Business Rule Violations
  2. Data Validation Errors
  3. Application Exceptions

Error Handling

Regardless of the origination of an error, an Angular application needs to handle the error. Angular has an ErrorHandler that is provided to the application when the application is initialized. This ErrorHandler will eventually catch and handle all thrown errors.

import {ERROR_ORIGINAL_ERROR, getDebugContext, getErrorLogger, getOriginalError} from './errors';

export class ErrorHandler {
  /**
   * @internal
   */
  _console: Console = console;

  handleError(error: any): void {
    const originalError = this._findOriginalError(error);
    const context = this._findContext(error);
    // Note: Browser consoles show the place from where console.error was called.
    // We can use this to give users additional information about the error.
    const errorLogger = getErrorLogger(error);

    errorLogger(this._console, `ERROR`, error);
    if (originalError) {
      errorLogger(this._console, `ORIGINAL ERROR`, originalError);
    }
    if (context) {
      errorLogger(this._console, 'ERROR CONTEXT', context);
    }
  }

  /** @internal */
  _findContext(error: any): any {
    if (error) {
      return getDebugContext(error) ? getDebugContext(error) :
                                      this._findContext(getOriginalError(error));
    }

    return null;
  }

  /** @internal */
  _findOriginalError(error: Error): any {
    let e = getOriginalError(error);
    while (e && getOriginalError(e)) {
      e = getOriginalError(e);
    }

    return e;
  }
}

export function wrappedError(message: string, originalError: any): Error {
  const msg = `${message} caused by: ${originalError instanceof Error ? originalError.message: originalError }`;
  const error = Error(msg);
  (error as any)[ERROR_ORIGINAL_ERROR] = originalError;
  return error;
}

The actual code for the Angular ErrorHandler contains comments and an example.

Provides a hook for centralized exception handling. The default implementation of ErrorHandler prints error messages to the console. To intercept error handling, write a custom exception handler that replaces this default as appropriate for your app.

The code sample provided shows that we can create our class that implements the ErrorHandler interface. A custom handler will need to override and provide a concrete implementation of the handleError() method.

class MyErrorHandler implements ErrorHandler {
  handleError(error) {
    // do something with the exception
  }
}

To allow a specific NgModule to use the custom Error Handler, use the providers configuration and the useClass property with the type of the new ErrorHandler.

Typically, you would provide the ErrorHandler for the entire application in the AppModule. However, each module can have its handler scoped at the module level.

@NgModule({
  providers: [{provide: ErrorHandler, useClass: MyErrorHandler}]
})
class MyModule {}

Errors from the Dark Side:: The Back End

Many times, the error is from the Back End of the application - the Web API. If an error occurs on the back end, you will typically get a 400 or 500 status code from the server. However, during the processing of an HTTP request, it is also possible to get an error. These errors may be connection related or an error in the processing of the HTTP request or the response. There is a lot of opportunities for things to go wrong.

For example, if you use HttpClient you can call the request() method. Using the rxjs pipe(), you can also use the catchError() which will return an HttpErrorResponse to be handled.

execute<T>(requestOptions: HttpRequestOptions): Observable<HttpResponse<ApiResponse<T>>> {
    try {
      return this.httpClient.request<T>(
        requestOptions.requestMethod.toString(),
        requestOptions.requestUrl,
        {
          headers: requestOptions.headers,
          observe: requestOptions.observe,
          params: requestOptions.params,
          reportProgress: requestOptions.reportProgress,
          withCredentials: requestOptions.withCredentials
        }
      ).pipe(
        catchError((errorResponse: any) => {
          return this.handleError(errorResponse);
        })
      );
    } catch (error) {
      this.handleError(error);
    }
  }

The HttpErrorResponse contains details to determine the source of the error. Was it from the server/http or from within the application. This will help you to determine what type of information to provide the user, if any. At a minimum, you could log this information to help monitor the health of the application and determine if any improvements should be made.

HttpErrorResponse: A response that represents an error or failure, either from a non-successful HTTP status - an error while executing the request, or some other failure which occurred during the
parsing of the response.

I updated the signature of the handleError() method to include either type of Error or HttpErrorResponse - this will allow for specialized handling based on the type of error.

protected handleError(error: Error | HttpErrorResponse): Observable<any> {
  if(error.error instanceof ErrorEvent)  {
    // A client-side or network error occurred. Handle it accordingly.
  } else {
      // The API returned an unsuccessful response.
  }
  // handler returns an RxJS ErrorObservable with a user-friendly error message. Consumers of the service expect service methods to return an Observable of some kind, even a "bad" one.
  // return throwError(error);
  return throwError(`Hey, you got my chocolate in your peanut butter.`);
}

Notice that the HttpErrorResponse type implements Error. Therefore, it contains information about the HTTP Request and also error information, like the stack trace if available.

class HttpErrorResponse extends HttpResponseBase implements Error {
  constructor(init: {...})
  get name: 'HttpErrorResponse'
  get message: string
  get error: any | null
  get ok: false

  // inherited from common/http/HttpResponseBase
  constructor(init: {...}, defaultStatus: number = 200, defaultStatusText: string = 'OK')
  get headers: HttpHeaders
  get status: number
  get statusText: string
  get url: string | null
  get ok: boolean
  get type: HttpEventType.Response | HttpEventType.ResponseHeader
}

The abstract base class for the HttpResponse provides the structure for other HTTP Response classes:

  • HttpErrorResponse
  • HttpHeaderResponse
  • HttpResponse
abstract class HttpResponseBase {
  constructor(init: {...}, defaultStatus: number = 200, defaultStatusText: string = 'OK')
  get headers: HttpHeaders
  get status: number
  get statusText: string
  get url: string | null
  get ok: boolean
  get type: HttpEventType.Response | HttpEventType.ResponseHeader
}

Custom Error Handler

Create a new class for the custom ErrorHandler.

ng generate class myErrorHandler --project=error-handling --spec=false
import { ErrorHandler } from "@angular/core";

export class MyErrorHandler implements ErrorHandler {
    handleError(error: any): void {
        throw new Error("Method not implemented.");
    }
}

Add a reference to the library module. We will need to import HttpClientModule. This will give us access to the ErrorHandler interface that we will need to implement.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
  imports: [
    CommonModule,
    HttpClientModule
  ]
})
export class ErrorHandlingModule {}

Implement the interface.

import { ErrorHandler } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';

export class MyErrorHandler implements ErrorHandler {
    handleError(error: Error | HttpErrorResponse): Observable<any> {
        throw new Error('Method not implemented.');
    }
}

The following implementation is doing a few things as a sample implementation.

  • uses a configuration service (injected); use to provide information on how to handle writing error events
  • uses a logging service (injected); used to allow the error handler to log information to a target
import { Injectable, ErrorHandler } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { noop } from 'rxjs';

import { ConfigurationService, ErrorHandlingConfig, IConfiguration, IErrorHandingConfig } from '@tc/configuration';
import { LoggingService, Severity } from '@tc/logging';

@Injectable({
  providedIn: 'root'
})
export class ErrorHandlingService extends ErrorHandler {
  serviceName = 'ErrorHandlingService';
  errorHandlingConfig: IErrorHandingConfig;
  hasSettings: boolean;

  constructor(private config: ConfigurationService, private loggingService: LoggingService) {
    super();

    this.init();
  }

  init() {
    // Use to provide default settings for error handling processing.
    this.errorHandlingConfig = new ErrorHandlingConfig('ErrorHandler', true);
    this.loggingService.log(this.serviceName, Severity.Warning, `Application [ErrorHandler] is using default settings`);

    // subscribe and use settings from the [ConfigurationService] when available.
    this.config.settings$.subscribe(settings => this.handleSettings(settings));
  }

  handleSettings(settings: IConfiguration) {
    this.errorHandlingConfig = settings.errorHandling;
    this.hasSettings = true;
    this.loggingService.log(this.errorHandlingConfig.name, Severity.Information, `Application [ErrorHandler] using configuration settings.`);
  }

  handleError(error: Error | HttpErrorResponse): any {
    if (this.errorHandlingConfig.includeDefaultErrorHandling) {
      // use the [super] call to keep default error handling functionality --> console;
      super.handleError(error);
    }

    if (this.hasSettings) {
      // A. HANDLE ERRORS FROM HTTP
      if (error instanceof HttpErrorResponse) {
        if (error.error instanceof ErrorEvent) {
          // A.1: A client-side or network error occurred. Handle it accordingly.
          const formattedError = `${error.name}; ${error.message}`;
          this.loggingService.log(this.errorHandlingConfig.name, Severity.Error, `${formattedError}`);
        } else {
          // A.2: The API returned an unsuccessful response (i.e., 400, 401, 403, etc.).
          /**
           * The [HttpService] should return a response that is consumable by the caller
           * of the API. The response should include relevant information and error messages
           * in a format that is known and consumable by the caller of the API.
           */
          noop();
        }
      } else {
        // B. HANDLE A GENERALIZED ERROR FROM THE APPLICATION/CLIENT;
        const formattedError = `${error.name}; ${error.message}}`;
        this.loggingService.log(this.errorHandlingConfig.name, Severity.Error, `${formattedError}`, error.stack ? error.stack : null);
      }
    }
  }
}

One Error Handler for Different Types of Errors

A1: HttpErrorResponse & ErrorEvent Handling

The signature of the handleError method can be either of (2) types: Error or HttpErrorResponse. One of the first things to do is to determine if the origin of the error is an HttpErrorResponse or not. If it is of types HttpErrorResponse and ErrorEvent, then it is either an application/client or network related error. Therefore, we will write this error to the application log.

A2: HttpErrorResponse Handling (No Handling Required)

If it is of type HttpErrorResponse only, then the origin is most likely the application's API/application back end. Therefore, the application should be able to handle the response (event though it is in an error state), because the response should be in a well-known API response format. There is no additional processing by the ErrorHandler for these types of errors.

An API response should be well-defined and known to the consumers of the API. A typical response either success or failure should contain a common set of properties. The following is an example of a schema that can be used to provide response information to the application.

  • IsSuccess: A boolean value used to indicate if the result of the request is a success or not. This should be set to false if the HTTP status code is an error status.
  • Message: A general message regarding the request (i.e., "Successfully created a new account.").
  • StatusCode: A valid HTTP status code.
  • Timestamp: A value indicating the date and time of the response.
export abstract class ApiResponse<T> {
    IsSuccess: boolean;
    Message: string;
    StatusCode: number;
    Timestamp: Date;
  }

A success response will extend from the abstract base class ApiResponse<T>. The Data payload will be in a known and defined type. The JSON data payload should map to a specific model by the application.

import { ApiResponse } from './api-response';

/**
 * Use to define a successful API response. A successful response will
 * most likely include a payload of data (i.e., use the Data property). 
 */
export class SuccessApiResponse<T> extends ApiResponse<T> {
  Data: T;
}

A failure* response will also extend from the abstract base class ApiResponse<T>. Instead of having a Data payload, it will have a list of ApiErrorMessage items to provide additional information to the application. This may include a message that could be displayable to the user.

import { ApiResponse } from './api-response';
import { ApiErrorMessage } from './api-error-message';

/**
 * Use to provide error information from an API. You can also 
 * use this class to create a response with errors while doing
 * error handling.
 * 
 * Errors: is a list om [ApiErrorMessage] items that contain specific
 * errors for the specified request. 
 */
export class ErrorApiResponse<T> extends ApiResponse<T> {
  Errors: ApiErrorMessage[] = [];
}

The specified error message items should also be well-defined and known by the application.

export class ApiErrorMessage {
    id?: string;
    statusCode?: string;
    message: string;
    isDisplayable: boolean;

    /**
     * Use to create a new [ApiErrorMessage]
     * @param message The error from the API.
     * @param displayable Use to indicate if the error should be displayed to the user.
     * @param id An optional identifier for the error.
     * @param statusCode An optional status code for the specified error.
     */
    constructor(message: string, displayable: boolean, id: string | null, statusCode: string | null) {
      this.message = message;
      this.isDisplayable = displayable;
      if (id) {
        this.id = id;
      }
      if (statusCode) {
        this.statusCode = statusCode;
      }
    }
  }

B: General Error from the Application/Browser Client

This type of error requires handling (i.e., logging to a centralized repository and/or console log). These are most likely errors that should be monitored and reviewed by stakeholders of the application.

  handleError(error: Error | HttpErrorResponse): any {
    if (this.errorHandlingConfig.includeDefaultErrorHandling) {
      // use the [super] call to keep default error handling functionality --> console;
      super.handleError(error);
    }

    if (this.hasSettings) {
      // A. HANDLE ERRORS FROM HTTP
      if (error instanceof HttpErrorResponse) {
        if (error.error instanceof ErrorEvent) {
          // A.1: A client-side or network error occurred. Handle it accordingly.
          const formattedError = `${error.name}; ${error.message}`;
          this.loggingService.log(this.errorHandlingConfig.name, Severity.Error, `${formattedError}`);
        } else {
          // A.2: The API returned an unsuccessful response (i.e., 400, 401, 403, etc.).
          /**
           * The [HttpService] should return a response that is consumable by the caller
           * of the API. The response should include relevant information and error messages
           * in a format that is known and consumable by the caller of the API.
           */
          noop();
        }
      } else {
        // B. HANDLE A GENERALIZED ERROR FROM THE APPLICATION/CLIENT;
        const formattedError = `${error.name}; ${error.message}}`;
        this.loggingService.log(this.errorHandlingConfig.name, Severity.Error, `${formattedError}`, error.stack ? error.stack : null);
      }
    }
  }

To use MyErrorHandler as the error handler (instead of Angular's default), update the application's AppModule with a provider item that uses the new class.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ErrorHandler } from '@angular/core';

import { AppComponent } from './app.component';
import { NxModule } from '@nrwl/nx';
import { RouterModule } from '@angular/router';
import { MyErrorHandler } from '@my/error-handling';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    NxModule.forRoot(),
    RouterModule.forRoot([], { initialNavigation: 'enabled' })
  ],
  providers: [
    {
      provide: ErrorHandler,
      useClass: MyErrorHandler
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

References

Resources

Discussion

pic
Editor guide
Collapse
papiermache profile image
papiermache

Nice article, but what is "@tc" ?

Collapse
jwp profile image
John Peters

Great work Matt. Tx