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..
Want to use code from this post? Check out the license.
Reader Comments
great as always...
question, I see that LoggingErrorHandler implements ErrorHandler.
When a class implements an interface, it promises to provide the behavior published by that interface.
And since you are not extending ErrorHandler (just implementing the interface) I am wondering how public handleError( error: any ) : void { ... is getting called for you?
regards
Sean
@Sean,
The way I understand it, we are telling the root module to associate our custom class - LoggingErrorHandler - with the Dependency-Injection token - ErrorHandler. This way, when Angular goes to inject the `ErrorHandler` token internally to the framework, it will actually receive our instance of `LoggingErrorHandler`.
Now, presumably, it then has some sort of try-catch block or Zone.js implementation that routes errors through the `.handleError()` method call. But, I don't know the inner bowels of the framework itself that well. Much hard to poke around than it was in NG 1.x.
understood, ya its kind of black magic (non standard as normally you would extend the class) but hey, it works!!! tx as always...
I will also post the Q on ng2 gitter, will see what the experts say... tx!
@Sean,
Also, you can always inject the `ErrorHandler` token inside your other services if you want to explicitly handle something in a try-catch. Like something that happens outside the normal Zone.js instance for Angular:
try {
. . . some dangerous code outside Angular . . . .
} catch ( error ) {
. . . errorHandler.handleError( error );
}
... though maybe there are more appropriate APIs for Zone-integration - I only understand Zones at the most abstract level.
got it, tx!!!
Hi, great write-up as always, I've implemented very similar and got it working, but I'm having trouble working out how to implement a view test (i.e. testing your `AppComponent` in jasmine)
My related stackoverflow issue: http://stackoverflow.com/questions/40494865/how-can-i-view-test-an-angular-2-errorhandler-override-class
Any pointers very welcome!
Hi, thanks for this nice article.
One thing I found out is that the solution as is does not work with the AOT compiler.
After a lot of time experimenting and trying to isolate the error, I found out that AOT does not seem to like the token you use for the options. Besides, I had to change the interface for the options to a class. Here are my (most important) changes:
>>>
export class LoggingErrorHandlerOptions {
rethrowError: boolean;
unwrapError: boolean;
}
export const LOGGING_ERROR_HANDLER_OPTIONS_TOKEN = new OpaqueToken('error.handler.options');
<<<
I want to open a modal dialog (ng2-bootstrap/ng2-bootstrap) showing exception with custom text on error this.errorLogDirective.show(this.findOriginalError(error)) that modal is not opened
UI doesn't update after exception is thrown, what could be the possible solution please suggest. this is because perhaps change detection does not work properly
@Marc,
Is it possible to post your full implementation somewhere of getting this to work with AoT? I'm still having a hard time wrapping my head around OpaqueTokens in Angular4.
Hi Ben,
I am getting the following errors,
1) at the time of calling handleError() function , compiler says handleError is not fuction.
2) at time of production build (ng build --prod) , ended up with following error
ERROR in Internal error: unknown identifier {"reference":{"rethrowError":false,"unwrapError":false}}
ERROR in ./src/main.ts
Module not found: Error: Can't resolve './$$_gendir/app/app.module.ngfactory' in 'C:some local path \src'
@ ./src/main.ts 7:0-74
@ multi ./src/main.ts
@Venkata,
I have the same problem
@Venkata,
Did you ever solve this issue?
I changed to:
export abstract class LoggingErrorHandlerOptions {
rethrowError: boolean;
unwrapError: boolean;
}
export class LOGGING_ERROR_HANDLER_OPTIONS implements LoggingErrorHandlerOptions {
rethrowError = false;
unwrapError = false;
};
I was using abstract classes instead of interfaces.
You can read more here:
https://stackoverflow.com/questions/42422549/angular-2-injectable-interface
Hi Ben,
Thanks for the article. Does this work with Angular v4 or v5 as well? Or do we need to make any changes?
@Zeshan,
I haven't tried re-compiling this; but, looking over it, I don't see any obvious red-flags in terms of compatibility. As such, I think you would be good to go.
@Tom,
Using an Abstract class an injectable is definitely a solid move. I have found that works really well when you need to create different implementations of a service. I haven't used it for data-only type instance; but, the theory should be the same.
Sounds like this is causing issues with AoT (Ahead of Time) compiling. Unfortunately, I don't really have any experience with AoT. I should start playing around with the Angular CLI - which I think uses AoT. Sorry I can't be more help on the matter at this time.
It looks like there might a discussion around this topic in the Angular Issues: https://github.com/angular/angular/issues/12631
@Ben,
Thanks Ben
Not tested, but moving to an InjectionToken may resolve this e.g.
And use that in the NgModule providers array: