Catch all errors in Angular app

Catch all possible errors using both global and angular error hooks.

If a tree raises an exception in the forest and there is no one to catch it, do you have a problem?

One of the best tools I have discovered this year is Sentry error reporting. It is available for multiple platforms, but I am using it mostly to catch and report client-side JavaScript exceptions.

The Sentry client library is called Raven and it has two features:

1: one can use the Raven to send an error with additional information to Sentry webservice

1
2
3
4
5
try {
doSomething();
} catch(e) {
Raven.captureException(e)
}

2: Raven can install global exception catcher for most modern browsers to intercept uncaught errors.

1
2
3
4
5
6
7
8
<script src="//cdn.ravenjs.com/1.1.2/jquery,native/raven.min.js"></script>
<script>
// DSN is your projects API key
Raven.config(<DSN>, {}).install();
</script>
<script>
foo.bar(); // this error will be reported to Sentry
</script>

Sentry allowed us to discover several errors we would have never found using more conventional unit or end 2 end testing, and these errors would never be reported by users.

In this post I will describe how to integrate Sentry error reporting with Angular code to catch all possible errors in addition to the global error handler installed by Raven.

Report errors that happen inside Angular code

Angular has a global $exceptionHandler factory that catches errors that happen inside controllers, services, etc. The default implementation just prints the error to the console and rethrows it. You can create a utility module to be reused in your apps that forwards the error to Sentry

1
2
3
4
5
6
7
8
angular.module('ErrorCatcher', [])
.factory('$exceptionHandler', function () {
return function errorCatcherHandler(exception, cause) {
console.error(exception.stack);
Raven.captureException(exception);
// do not rethrow the exception - breaks the digest cycle
};
});

Make your top level app depend on ErrorCatcher and any exceptions in angular code will be sent to Sentry.

Note the second argument 'cause'. Usually it is undefined, but sometimes it might have a value usually if the exception happens while parsing text. For example the link function sends the offending element's starting tag to the exception handler as the second argument

1
2
3
4
5
6
7
function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) {
try {
linkFn(scope, $element, attrs, controllers, transcludeFn);
} catch (e) {
$exceptionHandler(e, startingTag($element));
}
}

You can test errors thrown inside the Angular digest loop by executing from the browser console

1
2
angular.element(document.body).injector().get('$rootScope')
.$apply(function () { throw new Error('from angular'); });

Instead of document.body you can subsitute the selected element using angular.element($0). You can also store this code snippet together with other code snippets.

Report AJAX errors

Error responses from the server can be intercepted and reported to Sentry by installing $http response interceptor. Add your own interceptor factory to the ErrorCatcher module defined above:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
angular.module('ErrorCatcher', [])
.factory('errorHttpInterceptor', ['$q', function ($q) {
return {
responseError: function responseError(rejection) {
Raven.captureException(new Error('HTTP response error'), {
extra: {
config: rejection.config,
status: rejection.status
}
});
return $q.reject(rejection);
}
};
}])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('errorHttpInterceptor');
}]);

Now you will know about error server responses, which to your users look like errors.

Do not report cancelled requests

If the user browses to a different page while the AJAX request is pending, the request gets cancelled and trips the error handler. To avoid reporting the cancelled requests as errors, check the rejection.status property. If it is undefined, the request was cancelled and should not be reported.

Correctly throwing an error

Two tips on throwing an error:

1: Always throw an instance of Error class, never throw a string or an object. Getting stack trace is only possible via Error object, for example.

1
2
throw new Error('broken')  // good
throw 'broken' // bad

2: Throwing an error stops code execution. If the error is not serious enough, throw it asynchronously

1
2
3
4
5
if (!some_condition) {
setTimeout(function () {
throw new Error('some_condition failed!');
}, 0);
}

You could even create an utility function for this purpose

1
2
3
4
5
6
7
8
9
function asyncThrow(err) {
if (!(err instanceof Error)) {
throw new Error('please throw only instance of Error, not ' + err);
}
setTimeout(function () { throw err; }, 0);
}
if (!some_condition) {
asyncThrow(new Error('some_condition failed!'));
}

I prefer to use defer function available in Lodash and similar libraries. It runs the supplied function by scheduling it onto even loop (just like setTimeout). Using defer makes the async intent more clear that using setTimeout

1
2
3
if (!some_condition) {
_.defer(function () { throw new Error('some_condition failed!'); });
}

Make sure to use a descriptive message, since the error's stack will probably be incorrect due to async throw.

Bonus 1

I wrote useful-express-partials which includes a Jade template for setting up Raygun error reporting.

Related