AngularJS provides an application exception handling ($exceptionHandler ) service that traps unhandled errors. The default implementation of this service simply delegates the exception to the $log service which just displays the error on the browser console. As we can imagine, the user does not view these errors, and since the data is not collected, there may be some valuable information that goes unnoticed.
In order to help us improve the quality of our app, we need to add client side instrumentation that can enable us to track exceptions and send them to a centralized location for inspection. Since AngularJS already sends uncaught exception to the $exceptionHandler service, we can override this service and provide an implementation that can help us capture and report these errors to a server service.
Server Logger ($svcLog)
In order to send the client errors, we first need to create a service that can be used for server side logging. This service just needs to accept an object with the exception information and call the corresponding API. This is the service that is injected into our implementation of $exceptionHandler, so we are able to handle and send the error. Notice in the code below how we are using JQuery Ajax and not the $http service. The reason behind this is problem with circular reference. We cover this concern in a later section.
var app = angular.module("app", []); app.factory('$svcLog', [svcLog]); function svcLog(){ var svc = { add: add } function add(exception){ //simulate sending the error here var data = angular.toJson(exception); console.log ('Sending to the server - ' + data ); $.ajax({ type: "POST", url: "/api/log", contentType: "application/json", data: data}); } return svc; } |
Overriding $exceptionHandler
We can override this service by declaring a factory with the same name. In this factory, we can inject the $svcLog service that we created. This way this works is that AngularJS delegates all the unhandled error calls by calling the service handler function. In this function, we get the exception object. For our simple logging service, we are just passing the message and stack information from the exception object. We also want to continue to log the error to the console window and allow the application to continue its execution. To enable that, we need to call $log.error.apply($log, arguments).
app.factory('$exceptionHandler', ['$log','$svcLog',svcExceptionHandler]); function svcExceptionHandler($log, $svcLog) { var handler = function (exception, cause) { var ex = { message: exception.message, stack:exception.stack }; //log to console and allow the app to continue $log.error.apply($log, arguments); try { //send the error $svcLog.add(ex); } catch (err) { $log.log(err); } }; |
Create an Error
We are now ready to simulate an error on our application. In our home controller, we are going to make references to an undefined object. This should cause an error on our application. AngularJS should trap this unhandled error and delegate it to our implementation of $excetionHandler which sends the error to our server. The createError function is called by a button on our view. We are also handling an app-error broadcast message which can enable us to display the information on the controller.
app.controller("ctrlHome", ["$scope", function($scope) { console.log('controller init'); $scope.createError = function(){ invalid.data = 'this is an error on the controller'; } $scope.$on('app-error', function (event, args) { $scope.error = args.message; $scope.stack = args.stack; }); } ]); |
About Circular Reference
We need to keep in mind that depending on the implementation of our $svcLog service, we may run into a circular reference exception. If we change our implementation of $svcLog to use the $http service, we will get this error:
$rootScope <- $http <- $svcLog <- $exceptionHandler <- $rootScope
To get around this problem, we just need to inject the $injector service into our $exceptionHandler service and then inject our $svcLog using code. This is a way to force an injection without the circular reference. To show this, we create the $svcLogHttp service which uses the $http service to make the AJAX call.
app.factory('$exceptionHandler', ['$log', '$injector', svcExceptionHandler]); app.factory('$svcLogHttp', ['$http', svcLogHttp]); function svcLogHttp($http){ var svc = { add: add } function add(exception){ //simulate sending the error here $http.post('/api/log', exception).then(function (resp) { console.log(); }, function (err) { console.log(err); }); } return svc; } //$svcLog is undefined. function svcExceptionHandler($log, $injector, $svcLog) { var $svc = $svcLog; var handler = function (exception, cause) { var ex = { message: exception.message, stack: exception.stack }; //log to console and allow the app to continue $log.error.apply($log, arguments); try { if (!$svc) { $svc = $injector.get('$svcLogHttp'); } //send the error $svc.add(ex); } catch (err) { $log.log(err); } }; return handler; } |
Demo
(Open Dev tools to see console messages)
As we can see on this demo, we are not only maintaining the same behavior by logging into the console and allowing the application to run, but we are also able to send (simulate as there is no server accepting those requests - 404 error on console) the messages to our server which enable us to discover some interesting stuff that may be happening with our application.
Originally published by ozkary.com