Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with:

Logging Client-Side Errors With AngularJS And Stacktrace.js

By Ben Nadel on

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.


 
 
 

 
  
 
 
 

View this demo in my JavaScript-Demos project on GitHub.

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:

Error: Circular dependency: $http <- errorLogService <- $exceptionHandler <- $rootScope

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.




Reader Comments

@Christian,

That looks pretty cool - I haven't seen that before. How do you pipe errors into it? Does it expose some local api, like sentry.log()? The public docs don't seem to have any actual code in them.

Reply to this Comment

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.

Reply to this Comment

@Eric,

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?

Reply to this Comment

@Sami,

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.

Reply to this Comment

@Ben,

If I get an anonymous function on the stack, and the file it's in is on the same domain as the current domain I get the script source so I can parse the line and perhaps guess the function name.

This is used in the case where you define a function like this: var foo = function() {..} because the function "name" is anonymous according to the runtime.

Reply to this Comment

@Eric,

Ahh, very clever! For future reference, I should probably name my functions, even if they are simply being passed into other methods, ex:

thing.do( function myCallback() { .. } );

That would probably make the debugging easier in general!

Reply to this Comment

If you want to keep the default logging of $exceptionHandler while extending it with custom functionality, you can use a decorator:

angular.module('myApp').config(['$provide', function ($provide) {
$provide.decorator('$exceptionHandler', ['$delegate', function ($delegate) {
return function(exception, cause) {
// Calls the original $exceptionHandler.
$delegate(exception, cause);

//Custom error handling code here.
};
}]);
}]);

Reply to this Comment

@Joe,

Very interesting! I have not used decorators in AngularJS before. I'll definitely have to check that out. Thanks for the insight!

Reply to this Comment

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.

Reply to this Comment

@Sisb,

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.

Reply to this Comment

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.

I have an example of a couple throttling functions here:
https://github.com/TrackJs/Tech-Demo/blob/master/src/TrackJs.Demo/Content/js/transmission.js

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.

Reply to this Comment

@Todd,

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.

Reply to this Comment

Great post Ben! Did you consider using JSNLog (js.jsnlog.com) to take care of the logging itself?

It has functionality to send your log data to the server and also to throttle log data so you don't get overwelmed. Plus named loggers, etc. Log4J style.

Reply to this Comment

You could use Atatus to log error from AngularJS.

http://www.atatus.com/

In the docs, under exceptions you can see how it can hook into AngularJS

angular.module('app').factory('$exceptionHandler', function () {
return function (exception, cause) {
throw exception;
};
});

We successfully use Atatus itself to monitor www.atatus.com website.

Reply to this Comment

I feel that falling back on jQuery to do a AJAX request is overkill. So instead, to avoid circular dependency problems I inject the services upon the first retrieval of the error handler.

By slightly changing the error log service you can use $http without any problems.

function( $log, $window, $injector, stacktraceService ) {
var $http = null;
// I log the given error to the remote server.
function log( exception, cause ) {
if (!$http)
try { $http = $injector.get('$http'); }
catch (e) {
$http = null; // To assure a retry on the next error
$log.warn('Retrieving $http service for error logging failed.');
$log.log(e);
}

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.)

Reply to this Comment

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.

Reply to this Comment

Thanks for this post. Client side logging is a must in my opinion.

I too used the jQuery ajax approach. However, on my current project I wanted to remove the jQuery dependency. The mailing list suggested I could use the $httpBackend directly.

Here is the post: http://stackoverflow.com/questions/22696767/how-can-i-call-angulars-httpbackend-directly/22696844#22696844

<pre>
$httpBackend('POST', '/some/url', //method and url
JSON.stringify(buildLogInfo()), //request body
function(status,resp,headerString){ //response call back
console.log('manual backend call',status,resp,headerString);
},
{"Content-Type": "application/json"} //request headers
);
</pre>

Reply to this Comment

Thanks for the post! This is a great tool combination.

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?

Thanks!

Reply to this Comment

@Aaven,

You could do something like that with a tool that supports source maps. stacktrace.js doesn't support source maps yet, and I'm not sure of another way to do this.

Reply to this Comment

Is there a way to figure out which controller is throwing the exception at $exceptionHandler?

Reply to this Comment

How would you implement this in a scenario where you are using require.js and manually boostrapping your app?

Reply to this Comment

When i try to implememt this code in my proejct i am getting below error

$ is not defined
Error: [$rootScope:inprog] $digest already in progress

at the line
$.ajax({
type: "POST",
url: window.apirooturl + "/logger",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: errorMessage,
type: "exception",
stackTrace: stackTrace,
cause: ( cause || "")
})
});

can anyone help in this ?/

Reply to this Comment

This is a great and I was able to get this implemented, however it is breaking jasmine tests with expect(fn).toThrow() matcher, I get test FAILED 'Error: Expected function to throw an exception'.

I've spent hours now and I cannot figure out why, as an exception is getting thrown in application as expected, any ideas?

Reply to this Comment

@ben: thx for the article :)
Did you find a solution to use the $http service and stacktrace.js at the same time? That will be great!

Reply to this Comment

Hi Ben,

Can you share the code for how you handle the error message in your server side, where you have posted the message through jQuery....

Reply to this Comment

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).

Any workaround or hooks back into the original scope?

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.