Creating A Custom ErrorHandler In Angular 2 RC 6

I know that I've looked at logging errors in Angular 2 before; but, in the recent release of RC 6, I noticed that there was a breaking change, replacing the ExceptionHandler service with the ErrorHandler service. And, since my understanding of Angular 2 is a few RC's behind the most current release, I thought error handling would be an easy topic on which to try and close the gap in my understanding.

Run this demo in my JavaScript Demos project on GitHub.

There's not much point in overriding the core exception handler - ErrorHandler - unless we are going to do something meaningful with those error. In this case, we're going to capture them and then send them to several points of error aggregation; we'll track them on the server as well as send them to various Software-as-a-Service trackers like NewRelic and Raygun.

NOTE: Normally, you'll only use one SaaS option for error tracking; but, in this demo, I'm using several just to help ignite the fires of inspiration.

We could build this level of tracking right into our custom ErrorHandler implementation; but, I want to try and keep a clean separation of responsibilities. So, instead of overloading the ErrorHandler with too much logic, we're going to create a separate ErrorLogService that manages the logging and aggregation of errors. This class will expose one public method, .logError().

// In order to get the TypeScript compiler to not complain about unknown variables, // I'm declaring these various services as ambient values. // -- // WARNING: I'm not so good at using TypeScript yet - I am sure there is a way to do // this in some global declarations file; but, I don't know how to do that yet. declare var newrelic: { noticeError( error: any ) : void; }; declare var Raygun: { send( error: any ) : void; } declare var Rollbar: { error( error: any ) : void; } declare var trackJs: { track( error: any ) : void; } // Import the core angular services. import { Http } from "@angular/http"; import { Injectable } from "@angular/core"; import { Response } from "@angular/http"; @Injectable() export class ErrorLogService { private http: Http; // I initialize the service. constructor( http: Http ) { this.http = http; } // --- // PUBLIC METHODS. // --- // I log the given error to various aggregation and tracking services. public logError( error: any ) : void { // Internal tracking. this.sendToConsole( error ); this.sendToServer( error ); // Software-as-a-Service (SaaS) tracking. // -- // NOTE: These are all here as an example - you wouldn't actually be using all // of these in the same application. this.sendToNewRelic( error ); this.sendToRaygun( error ); this.sendToRollbar( error ); this.sendToTrackJs( error ); } // --- // PRIVATE METHODS. // --- // I send the error the browser console (safely, if it exists). private sendToConsole(error: any): void { if ( console && console.group && console.error ) { console.group( "Error Log Service" ); console.error( error ); console.error( error.message ); console.error( error.stack ); console.groupEnd(); } } // I send the error to the NewRelic error logging service. private sendToNewRelic( error: any ) : void { // Read more: https://docs.newrelic.com/docs/browser/new-relic-browser/browser-agent-apis/report-data-events-browser-agent-api newrelic.noticeError( error ); } // I send the error to the Raygun error logging service. private sendToRaygun( error: any ) : void { // Read more: https://raygun.com/raygun-providers/javascript Raygun.send( error ); } // I send the error to the Rollbar error logging service. private sendToRollbar( error: any ) : void { // Read more: https://rollbar.com/docs/notifier/rollbar.js/api/ Rollbar.error( error ); } // I send the error to the server-side error tracking end-point. private sendToServer( error: any ) : void { this.http .post( "./error-logging-endpoint", // Doesn't really exist in demo. { type: error.name, message: error.message, stack: error.stack, location: window.location.href } ) .subscribe( ( httpResponse: Response ) : void => { // ... nothing to do here. }, ( httpError: any ) : void => { // NOTE: We know this will fail in the demo since there is no // ability to accept POST requests on GitHub pages. // -- // console.log( "Http error:", httpError ); } ) ; } // I send the error to the Track.js error logging service. private sendToTrackJs( error: any ) : void { // Read more: http://docs.trackjs.com/tracker/framework-integrations#angular trackJs.track( error ); } }

As you can see, the .logError() method turns around a delegates to several private methods that each handle distributing the error to a different end-point.

CAUTION: In order to prevent the in-browser TypeScript compiler from complaining about my SaaS client libraries, I am declaring them as ambient values at the top of the file. I am sure there is a more intelligent way to do this; but, I am not particularly good with the TypeScript just yet.

Once we have our logging service defined, we need to start sending it errors. This is where our custom ErrorHandler implementation comes into play. We're going to override the core ErrorHandler and replace it with a version that both logs the error to the console (the default behavior) and sends the error to our new ErrorLogService.

If you look at the core implementation of the ErrorHandler, you will notice a few things:

Errors in Angular 2 appear to be "wrapped" in a sub-class of the Error object.

There is an option to rethrow errors that are passed into the ErrorHandler.

The ErrorHandler is instantiated using a provider factory.

I don't want to use a Factory to create our custom implementation. In general, I try to use "standard" dependency management through class references instead of factory functions; I find that factory functions make it a bit harder to tweak implementation details.

But, at the same time, I do want to be able to provide some custom configuration options on whether or not we rethrow errors and / or try to unwrap them before logging them. To accomplish this, I am going to provide a dependency-injection (DI) token - LOGGING_ERROR_HANDLER_OPTIONS - that includes configuration options for our custom implementation.

The developer may or may not override these options. Which means that we have to provide a default implementation of the configuration options. And, since we now need to provide both the custom ErrorHandler implementation as well as the default options implementation, we'll wrap both of these up in a single providers collection (at the bottom of the file): LOGGING_ERROR_HANDLER_PROVIDERS.

// Import the core angular services. import { ErrorHandler } from "@angular/core"; import { forwardRef } from "@angular/core"; import { Inject } from "@angular/core"; import { Injectable } from "@angular/core"; // Import the application components and services. import { ErrorLogService } from "./error-log.service"; export interface LoggingErrorHandlerOptions { rethrowError: boolean; unwrapError: boolean; } export var LOGGING_ERROR_HANDLER_OPTIONS: LoggingErrorHandlerOptions = { rethrowError: false, unwrapError: false }; @Injectable() export class LoggingErrorHandler implements ErrorHandler { private errorLogService: ErrorLogService; private options: LoggingErrorHandlerOptions; // I initialize the service. // -- // CAUTION: The core implementation of the ErrorHandler class accepts a boolean // parameter, `rethrowError`; however, this is not part of the interface for the // class. In our version, we are supporting that same concept; but, we are doing it // through an Options object (which is being defaulted in the providers). constructor( errorLogService: ErrorLogService, @Inject( LOGGING_ERROR_HANDLER_OPTIONS ) options: LoggingErrorHandlerOptions ) { this.errorLogService = errorLogService; this.options = options; } // --- // PUBLIC METHODS. // --- // I handle the given error. public handleError( error: any ) : void { // Log to the console. try { console.group( "ErrorHandler" ); console.error( error.message ); console.error( error.stack ); console.groupEnd(); } catch ( handlingError ) { console.group( "ErrorHandler" ); console.warn( "Error when trying to output error." ); console.error( handlingError ); console.groupEnd(); } // Send to the error-logging service. try { this.options.unwrapError ? this.errorLogService.logError( this.findOriginalError( error ) ) : this.errorLogService.logError( error ) ; } catch ( loggingError ) { console.group( "ErrorHandler" ); console.warn( "Error when trying to log error to", this.errorLogService ); console.error( loggingError ); console.groupEnd(); } if ( this.options.rethrowError ) { throw( error ); } } // --- // PRIVATE METHODS. // --- // I attempt to find the underlying error in the given Wrapped error. private findOriginalError( error: any ) : any { while ( error && error.originalError ) { error = error.originalError; } return( error ); } } // I am the collection of providers used for this service at the module level. // Notice that we are overriding the CORE ErrorHandler with our own class definition. // -- // CAUTION: These are at the BOTTOM of the file so that we don't have to worry about // creating futureRef() and hoisting behavior. export var LOGGING_ERROR_HANDLER_PROVIDERS = [ { provide: LOGGING_ERROR_HANDLER_OPTIONS, useValue: LOGGING_ERROR_HANDLER_OPTIONS }, { provide: ErrorHandler, useClass: LoggingErrorHandler } ];

As you can see, the LOGGING_ERROR_HANDLER_PROVIDERS contains both the reference to our custom LoggingErrorHandler class and our default options. Our custom error handler implements the one required method - handleError() - which, internally, looks at the injected options to see how to consume the given error object.

I am leaving the "unwrapping" of errors as a custom option - turned off by default - since the wrapping of errors is not a publicized matter. And, since the wrapped errors are actually sub-classes of the native Error type, the wrapping itself should be complete transparent.

Once we have our custom ErrorHandler implementation, we have to tell Angular 2 that we want to use our implementation instead of the core implementation. To do this, we have to override the ErrorHandler token in our root NgModule providers:

// Import the core angular services. import { BrowserModule } from "@angular/platform-browser"; import { HttpModule } from "@angular/http"; import { NgModule } from "@angular/core"; // Import the application components and services. import { AppComponent } from "./app.component"; import { ErrorLogService } from "./error-log.service"; import { LOGGING_ERROR_HANDLER_PROVIDERS } from "./logging-error-handler"; import { LOGGING_ERROR_HANDLER_OPTIONS } from "./logging-error-handler"; @NgModule({ bootstrap: [ AppComponent ], imports: [ BrowserModule, HttpModule ], declarations: [ AppComponent ], providers: [ ErrorLogService, // CAUTION: This providers collection overrides the CORE ErrorHandler with our // custom version of the service that logs errors to the ErrorLogService. LOGGING_ERROR_HANDLER_PROVIDERS, // OPTIONAL: By default, our custom LoggingErrorHandler has behavior around // rethrowing and / or unwrapping errors. In order to facilitate dependency- // injection instead of resorting to the use of a Factory for instantiation, // these options can be overridden in the providers collection. { provide: LOGGING_ERROR_HANDLER_OPTIONS, useValue: { rethrowError: false, unwrapError: false } } ] }) export class AppModule { // ... nothing to do here. }

In addition to the service provider, I'm also outlining what it would be like to override the service options; but, in this case, I'm just redefining the default options.

Now, we can test this by creating a root component that throws an error:

// NOTE: I'm just declaring the non-existing function so that TypeScript doesn't // yell at me. declare var promoteSynergy: any; // Import the core angular services. import { Component } from "@angular/core"; @Component({ selector: "my-app", template: ` <p> <a (click)="trigger()">Trigger an Error</a>, like a boss. </p> ` }) export class AppComponent { // I initialize the component. constructor() { } // --- // PUBLIC METHODS. // --- // I trigger an error (to test the custom ErrorHandler). public trigger() : void { // CAUTION: This method does NOT exist. promoteSynergy(); } }

As you can see, this component calls a method - promoteSynergy() - which doesn't exist. And when we try to run this app in the browser and call this method, we get the following console output:

In this case, both our custom LoggingErrorHandler() implementation and our ErrorLogService() log the error to the console. But, you can see that the ErrorLogService() also attempts to POST the error to the server (even though we have no active server in our demo).

I don't think I really covered anything particularly new in this post. Mostly, this was just an excuse for me to start using Angular 2 RC 6 and the new NgModule architecture. The nice thing about this exploration, however, was that I finally realized that the "wrapped" errors in Angular 2 are still instances of the native Error object, which means that they are still easy to consume. And, theoretically, should not cause any issue with SaaS services like NewRelic, Raygun, Rollbard, or TrackJS, etc..

Tweet This Deep thoughts by @BenNadel - Creating A Custom ErrorHandler In Angular 2 RC 6 Woot woot — you rock the party that rocks the body!







