June 3, 2015
Client-side Errors in Rich Internet Application-Architectures
Problem
When building applications with frameworks like AngularJS, we usually create so called “Rich Internet Applications” (RIA). The architecture of a RIA differs from a “traditional” application in several aspects:
- The server is (almost) stateless
- The client is stateful
- Server and client communicate over a coarse-grained interface
- The client contains (more) business logic
With good reason, AngularJS get’s hyped quite a lot and the blogosphere is full with articles explain how to implement a RIA architecture with AngularJS. AngularJS is particularly good at implementing e.g. the client state and business logic. Take for example the whole dependency injection mechanism and the support for test-driven development in the API.
However, one important aspect often gets overlooked, simply because we, the developers, are not used it: Dealing with errors on the client side. Why are we not used to it? In old/traditional architectures the UI state and business logic is is implemented on the server. Every (non-trivial) click, state transition, interaction etc. is handled on the server meaning unexpected errors, bugs, etc. are always caught and end up in our Logback/SLF4J/whatever log file. Often the default behavior is already enough (Servlet engine catches the error, Log library handles the error) and we rarely design this part of our application explicitly.
Back to AngularJS. As stated above, writing applications in AngularJS means writing business logic and UI state in the client. What if your code contains an error? What if one of your dependencies contain an error? Well, we will never know (unless you call your users and ask them check the browser’s JavaScript console, of course).
Solution
Closing this gap involves three tasks:
- Catching all errors on the client side
- Sending them to the server
- Logging them
This blog article will concentrate on the first item. Implementing item two and three should be obvious. However, for the second item, instead of sending error messages to your server, you could use services like:
These services are very nice and worth a look. However, I prefer to first collect all relevant log entries in one place and use a tool on top of this.
Catching AngularJS errors
Catching errors in AngularJS applications is quite simple due to the included dependency mechanism.
Option 1
The default module ng includes a $log service that is used by AngularJS itself and (hopefully) by your AngularJS libraries as well as your own code. Instead of writing something like
1 |
console.log("error in ..."); |
you would inject the $log service and write
1 |
$log.error("error in ..."); |
In AngularJS’ DI framework, you can easily override a provided services. We can use this to override the default $log implementation and to provide a custom service which sends the error message to our server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
angular.module('app').factory('$log', function ($window, $http) { return { log: function (message) { $window.console.log(message); }, info: function (message) { $window.console.log(message); }, warn: function (message) { $window.console.log(message); }, error: function (message) { $http.post('errorHandler', message); $window.console.log(message); }, debug: function (message) { $window.console.log(message); } }; }); |
In the error function, we additionally send the message to our server.
Option 2
The disadvantage of option 1 is that we loose information about the error, e.g. the stack strace. The solution is too hook into AngularJS a bit deeper by decorating the $exceptionHandler service:
1 2 3 4 5 6 7 8 9 |
angular.module('eh').config(function ($provide) { $provide.decorator('$exceptionHandler', function ($log, $delegate) { return function (exception, cause) { $http.post('errorHandler', createMessage(exception, cause)); $delegate(exception, cause); }; }); }); |
This allows us to process the exception message as well as the stack trace.
Catching generic JavaScript errors
Both options above work, because AngularJS takes care to catch the exceptions in our code. Possible sources are
- controller/service/directive construction function
- event handlers, e.g. ng-click
- callbacks in promises
- …
However, everything that happens outside of an AngularJS digest cycle/callback/etc. can’t be intercepted. For example, this could be a DOM callback function registered with jQuery. Fortunately, JavaScript allows us to register a global error handler:
1 2 3 4 |
window.onerror = function (msg, url, line) { $.post("errorHandler", createMessage(msg, url, line)); return false; }; |
The return code tells the Browser, if the error should be further processed (e.g. call the default handler).