Logging Client-Side Errors With AngularJS And Stacktrace.js

Last year, at cf.Objective(), I was watching Elliott Sprehn give a presentation on Production Ready JavaScript. In part of the presentation, he was talked about client-side errors and recommended that everyone log client-side errors to the server. To be honest, before he mentioned it, I don't think that it had ever occurred to me! Before that, I had only ever logged server-side errors. But, now that I am building single-page applications with AngularJS, client-side error logging has become an essential tool in creating a stable application. Getting the right error information isn't always easy, though; so, I thought I'd share what I do in my AngularJS applications.

AngularJS has excellent error handling! As long as you are inside of an AngularJS context or, you are executing code inside an $apply() callback, AngularJS will catch client-side errors, log them gracefully to the console, and then let your JavaScript application continue to run. The only problem with this is that the current user is the only one who knows that the error occurred.

To help me and my team iron our JavaScript errors, we needed to intercept the core AngularJS error handling and add a server-side communication aspect to it. To do this, we had to override the $exceptionHandler provider and replace it with a custom one that POST'ed the error to the server using AJAX (Asynchronous JavaScript and XML).

Posting the error to the server was only half the battle; it turns out that getting the right error information out of a JavaScript exception object is not super easy, especially across multiple browsers. Luckily, I found Stacktrace.js by Eric Wendelin. Stacktrace.js can take an error object and produce a stacktrace that works in every browser that we support. It has been an invaluable library!

In the following code, I've tried to isolate all of the error handling aspects of my AngularJS application. Really, the only part that I've left out is the debouncing. This code will blindly post every error that occurs on the client. In reality, however, this approach adds too much noise to the error log. As such, in production, I only post unique errors within a given time span. This way, if some directive on an ngRepeat, for example, throws an error, I don't end up making an HTTP request for every single ngRepeat item.

<!doctype html>

<html ng-app="Demo" ng-controller="AppController">

<head>

<meta charset="utf-8" />

<title>

Logging Client-Side Errors With AngularJS And Stacktrace.js

</title>

<style type="text/css">

a[ ng-click ] {

cursor: pointer ;

text-decoration: underline ;

}

</style>

</head>

<body>

<h1>

Logging Client-Side Errors With AngularJS And Stacktrace.js

</h1>

<p>

<a ng-click="causeError()">Cause Error</a>...

</p>

<p>

<em>

<strong>Note:</strong> Look at the JavaScript console to

see the errors being reported.

</em>

</p>

<!-- Load jQuery and AngularJS from the CDN. -->

<script

type="text/javascript"

src="../../vendor/jquery/jquery-2.0.3.min.js">

</script>

<script

type="text/javascript"

src="../../vendor/angularjs/angular-1.0.7.min.js">

</script>

<script

type="text/javascript"

src="../../vendor/stacktrace/stacktrace-min-0.4.js">

</script>

<script type="text/javascript">

// Create an application module for our demo.

var app = angular.module( "Demo", [] );

// -------------------------------------------------- //

// -------------------------------------------------- //

// The "stacktrace" library that we included in the Scripts

// is now in the Global scope; but, we don't want to reference

// global objects inside the AngularJS components - that's

// not how AngularJS rolls; as such, we want to wrap the

// stacktrace feature in a proper AngularJS service that

// formally exposes the print method.

app.factory(

"stacktraceService",

function() {

// "printStackTrace" is a global object.

return({

print: printStackTrace

});

}

);

// -------------------------------------------------- //

// -------------------------------------------------- //

// By default, AngularJS will catch errors and log them to

// the Console. We want to keep that behavior; however, we

// want to intercept it so that we can also log the errors

// to the server for later analysis.

app.provider(

"$exceptionHandler",

{

$get: function( errorLogService ) {

return( errorLogService );

}

}

);

// -------------------------------------------------- //

// -------------------------------------------------- //

// The error log service is our wrapper around the core error

// handling ability of AngularJS. Notice that we pass off to

// the native "$log" method and then handle our additional

// server-side logging.

app.factory(

"errorLogService",

function( $log, $window, stacktraceService ) {

// I log the given error to the remote server.

function log( exception, cause ) {

// Pass off the error to the default error handler

// on the AngualrJS logger. This will output the

// error to the console (and let the application

// keep running normally for the user).

$log.error.apply( $log, arguments );

// Now, we need to try and log the error the server.

// --

// NOTE: In production, I have some debouncing

// logic here to prevent the same client from

// logging the same error over and over again! All

// that would do is add noise to the log.

try {

var errorMessage = exception.toString();

var stackTrace = stacktraceService.print({ e: exception });

// Log the JavaScript error to the server.

// --

// NOTE: In this demo, the POST URL doesn't

// exists and will simply return a 404.

$.ajax({

type: "POST",

url: "./javascript-errors",

contentType: "application/json",

data: angular.toJson({

errorUrl: $window.location.href,

errorMessage: errorMessage,

stackTrace: stackTrace,

cause: ( cause || "" )

})

});

} catch ( loggingError ) {

// For Developers - log the log-failure.

$log.warn( "Error logging failed" );

$log.log( loggingError );

}

}

// Return the logging function.

return( log );

}

);

// -------------------------------------------------- //

// -------------------------------------------------- //

// I control the root of the application.

app.controller(

"AppController",

function( $scope ) {

// ---

// PUBLIC METHODS.

// ---

// I cause an error to be thrown in nested functions.

$scope.causeError = function() {

foo();

};

// ---

// PRIVATE METHODS.

// ---

function bar() {

// NOTE: "y" is undefined.

var x = y;

}

function foo() {

bar();

}

}

);

</script>

</body>

</html>

We aren't overriding the core AngularJS error handling so much as we are simply augmenting it. As you can see in the log() function, the very first thing we actually do is hand off the exception to AngularJS's native error() method. This way, the error is always logged to the Console even if our HTTP post fails.

You may also notice that the actual HTTP post to the server is executed using jQuery's $.ajax() method and not AngularJS's $http service. This is done on purpose; the $http service uses the errorLogService, so any attempt to use the $http service inside of our exception handler will cause a circular dependency:

When I added this code to my AngularJS applications, I'm embarrassed to say that I was shocked - completely shocked - at how many JavaScript errors were being generated. Many of the errors turn out to be cross-browser quirk; some turn out to be real bugs; and, some turn out to be complete mysteries that nobody on the team can produce. Slowly, however, we're trying to solve every client-side error that gets logged to the server.

Very nice! One thing I'd suggest is that you use the version of stacktrace.js on master because it has the very latest browser support (the version on the website is sometimes out-of-date). I plan on doing more frequent releases in the future, but the version on master is heavily tested and should be considered production-ready.

Ah, good to know! Actually, I do have one question for you. It looks like your code makes an HTTP request *sometimes* to re-get the Script source. What are the conditions for that? Is that for a particular browser?

While I haven't used Hoth personally, Aaron Greenlee has been working with me on some Amazon S3 stuff and he's talked about it. I'll have to dig into it a bit more. There's a lot that I'd love to clean up about my app.

I've been tackling this issue with a non-Angular application I'm developing. I am always interested in how people solve front-end error logging.I believe logging these errors isn't enough on their own, you need to use it alongside some analytical tracking. My personal tracking JS file keeps a record of document.referrer and window.location.href (amongst other things) and by using all this data I can trace the user's steps through my app to reproduce bugs.

I definitely agree with that. I wish I had a better system for it at the moment. Right now, I have a "log" that has a really simple UI that I can just page through. Ultimately, I hope to have zero errors; but for now, I just eye-ball it and see which ones are happening more often than not.

The great part of AngularJS is that many errors happen "behind the scenes" due to the way that AngularJS handles client-side errors.

Thanks for this post Ben! Something you should consider adding to your log is a throttling function. If the JavaScript code ends up in a loop condition, clients could quickly DoS your logging endpoint.

We have also just launched a JavaScript error tracking service called {Track:js} http://trackjs.com/ that attempts to provide some of this. I'd love to do something like this as an Angular plugin to our tracker!

{Track:js} approaches this problem a little different--rather than trying to wring more data out of the very-unhelpful JavaScript error, we instead capture analytics about what the user, the network, the console, and the environment were doing that led up to the error. I'd love to get your feedback on it. Shoot me an email and I'll give you an invite code.

I 100% agree. When I first implemented the client-side error logging, I didn't have any throttling. But, I quickly saw that if a client's code wasn't working, I could quickly get hundreds of errors from the same user in a matter of seconds.

Imagine an AngularJS directive that wasn't working on an ngRepeat that rendered 100 items (each causing a single error).

When I saw this happening, I did add throttling based on the error message and the time (a user can only log a given error once in a given time period). I included a note about "debouncing" in the demo code above; but, I felt that the actual implementation was probably beyond the scope of the blog post.

That said, I love the idea of trying to gather more information about what the user was actually doing at the time of the error. Right now, we do really just rely on the error itself and stacktrace (so that we can at least see which line of code caused the error). Of course, we do get times where the stacktrace is either unavailable; OR, that the error itself makes no sense and can't be reproduced on our end :(

I love the design of your site, btw. Very clean and clear! I'll definitely check it out.

You can use the same technique to load any other service you might need (such as ones that depend on $http). I use that to retrieve some information from other services that allow me to better trace errors. (For example you could retrieve the state of routing instead of just the URL.)

There are lots of ready-to-use tools to monitor js errors our there, it may be more convenient to use one of them.I am biased, we build js error monitoring tool, Qbaka, focusing on filtering noise from lots of event and providing only valuable information to developers.I would recommend to check out available services and choose the one that is best: most of tools provide basic features for free, might be better solution than just developing and running your own tool.

However, pardon my stupid question as I just started web programming, I am wondering if the tool still as valuable if the javascript files are optimized, i.e. uglified and minimized. I assume it will send out the traces for whatever optimized javascript file the error will almost always be at line 0.

So my real question is, is there an effective way to track remote javascript errors when the .js file is optimized? Or is it even possible?

So, When the log function is invoked, there is no scope. I think this is part of the security restrictions of stacktrace. For instance, in the log(exception,cause){...} if you alert(this) it says "undefined". Having no scope means we can't display our custom modal to the user (the only thing we can do is alert).

Great stuff. I'm incorporating this into our app now and I ran into a problem unit testing when you use JQueries $.ajax() method to post to the server.

The problem (obviously) is it's hard to test that it's working without actually posting during a unit test. Also, I don't want all my unit tests posting errors (some of which are intentional) to the log.

I found this solution and thought I might share it.

You can use the $injector service to get $http manually at runtime and avoid the circular dependency problem. Then in your unit tests, you can use $httpBackend to assert it's working as normal without actually posting.

Great post, thanks.Instead of stacktraceJs, I use JSN Log (jsnlog.com) which combines very well with Log4Net. It requires no setup serverside if you have Log4Net already installed. Which is awesome! No need to take care of handling the requests or anything.

Your post pointed me in the right direction on how to implement this with AngularJS.

Is this the best approach you've come across? We're thinking of adopting this idea and adding in some approaches to prevent abuse, but also wondering about existing third party integrations that may provide as an existing solution.

I found this nicearticle about how to to correctly use Bootstrap and Angular together:https://scotch.io/tutorials/how-to-correctly-use-bootstrapjs-and-angularjs-together

It's explained that we should not use jQuery when using AngularJs because the app will have a different behaviour,But to implement the authentication or use the http-auth-interceptor I find myself forced to use jQuery :-(

Can anyone give us a solution about integrating http-auth-interceptor in an angularJs app without using jQuery? That will be great :-)

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.