AngularJS - Stack trace ignoring source map AngularJS - Stack trace ignoring source map javascript javascript

AngularJS - Stack trace ignoring source map


Larrifax's answer is good but there is an improved version of the function documented in the same issue report:

.config(function($provide) {  // Fix sourcemaps  // @url https://github.com/angular/angular.js/issues/5217#issuecomment-50993513  $provide.decorator('$exceptionHandler', function($delegate) {    return function(exception, cause) {      $delegate(exception, cause);      setTimeout(function() {        throw exception;      });    };  });})

This will generate two stack traces, as Andrew Magee noted: one formatted by Angular, then a second one formatted by the browser. The second trace will apply sourcemaps. It's probably not a great idea to disable the duplicates, because you may have other Angular modules that also do work with exceptions that could be called after this via the delegation.


The only solution I could find is to bite the bullet and parse the source maps yourself. Here is some code that will do this. First you need to add source-map to your page. Then add this code:

angular.module('Shared').factory('$exceptionHandler', function($log, $window, $injector) {  var getSourceMappedStackTrace = function(exception) {    var $q = $injector.get('$q'),        $http = $injector.get('$http'),        SMConsumer = window.sourceMap.SourceMapConsumer,        cache = {};    // Retrieve a SourceMap object for a minified script URL    var getMapForScript = function(url) {      if (cache[url]) {        return cache[url];      } else {        var promise = $http.get(url).then(function(response) {          var m = response.data.match(/\/\/# sourceMappingURL=(.+\.map)/);          if (m) {            var path = url.match(/^(.+)\/[^/]+$/);            path = path && path[1];            return $http.get(path + '/' + m[1]).then(function(response) {              return new SMConsumer(response.data);            });          } else {            return $q.reject();          }        });        cache[url] = promise;        return promise;      }    };    if (exception.stack) { // not all browsers support stack traces      return $q.all(_.map(exception.stack.split(/\n/), function(stackLine) {        var match = stackLine.match(/^(.+)(http.+):(\d+):(\d+)/);        if (match) {          var prefix = match[1], url = match[2], line = match[3], col = match[4];          return getMapForScript(url).then(function(map) {            var pos = map.originalPositionFor({              line: parseInt(line, 10),               column: parseInt(col, 10)            });            var mangledName = prefix.match(/\s*(at)?\s*(.*?)\s*(\(|@)/);            mangledName = (mangledName && mangledName[2]) || '';            return '    at ' + (pos.name ? pos.name : mangledName) + ' ' +               $window.location.origin + pos.source + ':' + pos.line + ':' +               pos.column;          }, function() {            return stackLine;          });        } else {          return $q.when(stackLine);        }      })).then(function (lines) {        return lines.join('\n');      });    } else {      return $q.when('');    }  };  return function(exception) {    getSourceMappedStackTrace(exception).then($log.error);  };});

This code will download the source, then download the sourcemaps, parse them, and finally attempt to replace the locations in the stack trace the mapped locations. This works perfectly in Chrome, and quite acceptably in Firefox. The disadvantage is that you are adding a fairly large dependency to your code base and that you move from very fast synchronous error reporting to fairly slow async error reporting.


I just had the same issue and have been hunting around for a solution - apparently it's a Chrome issue with stack traces in general and happens to apply to Angular because it uses stack traces in error reporting. See:

Will the source mapping in Google Chrome push to Error.stack