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.

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.

Tweet This Great article by @BenNadel - Logging Client-Side Errors With AngularJS And Stacktrace.js Woot woot — you rock the party that rocks the body!







