Error monitoring in JavaScript is a thorny problem. On the one hand [CODE]window.onerror[/CODE] will at least notify you when something goes wrong. On the other, it won’t give you enough information to actually debug the problem. Notably absent in many cases is the stack trace.
That said, with a little bit of work it’s possible to get stacktraces that are reasonably complete in all browsers. Bugsnag’s JavaScript error monitoring uses the techniques below, ordered by effectiveness.
Modern Chrome and Opera (i.e. anything based around the Blink rendering engine) fully support the HTML5 draft spec for ErrorEvent and [CODE]window.onerror[/CODE]. In both of these browsers you can either use [CODE]window.onerror[/CODE], or (amazingly!) bind to the ‘error’ event properly:
-- CODE language-javascript --
// Only Chrome & Opera pass the error object.
window.onerror = function (message, file, line, col, error) {
console.log(message, "from", error.stack);
};
// Only Chrome & Opera have an error attribute on the event.
window.addEventListener("error", function (e) {
console.log(e.error.message, "from", e.error.stack);
});
Unfortunately Firefox, Safari, and IE are still around and we have to support them too. As the stacktrace is not available in [CODE]window.onerror[/CODE] we have to do a little bit more work.
It turns out that the only thing we can do to get stacktraces from errors is to wrap all of our code in a [CODE]try{ }catch(e){}[/CODE] block and then look at [CODE]e.stack[/CODE]. We can make the process somewhat easier with a function called wrap that takes a function and returns a new function with good error handling.
-- CODE language-javascript --
function wrap(func) {
// Ensure we only wrap the function once.
if (!func._wrapped) {
func._wrapped = function () {
try{
func.apply(this, arguments);
} catch(e) {
console.log(e.message, "from", e.stack);
throw e;
}
}
}
return func._wrapped;
};
This works. Any function that you wrap manually will have good error handling, but it turns out that we can actually do it for you automatically in most cases.
A new stack is created every time an event handler is called in JavaScript.This means you have to remember to wrap the function every time an event handler is called. Luckily in modern browsers (IE 10+) JavaScript uses prototype based inheritance.
Prototype based inheritance makes it easy to override a given function on lots of objects at the same time. The function we’re interested in is [CODE]addEventListener[/CODE], which is defined on the EventTarget prototype and inherited by DOM nodes, Windows, XMLHttpRequests and anything else that you can add an event listener to.
By changing the global definition of [CODE]addEventListener[/CODE] so that it automatically wraps the callback we can automatically insert [CODE]try{ }catch(e){}[/CODE] around most code. This lets existing code continue to work, but adds high-quality exception tracking.
-- CODE language-javascript --
var addEventListener = window.EventTarget.prototype.addEventListener;
window.EventTarget.prototype.addEventListener = function (event, callback, bubble) {
addEventListener.call(this, event, wrap(callback), bubble);
}
We also need to make sure that [CODE]removeEventListener[/CODE] keeps working. At the moment it won’t because the argument to [CODE]addEventListener[/CODE] is changed. Again we only need to fix this on the prototype object:
-- CODE language-javascript --
var removeEventListener = window.EventTarget.prototype.removeEventListener;
window.EventTarget.prototype.removeEventListener = function (event, callback, bubble) {
removeEventListener.call(this, event, callback._wrapped || callback, bubble);
}
Just to make things a little more tricky, in some browsers the class hierarchy is non-standard, so to make this work properly there are about twenty different prototypes to hook into. You can see the finished code here.
The above prototype trick coupled with similar handling of [CODE]setTimeout[/CODE] and friends gives us stack traces in all modern browsers. Regrettably there are still a large number (>10%) of people using old browser versions. It’d be nice to force such people to upgrade, but in the meantime we have one more trick up our sleeves.
In IE
-- CODE language-javascript --
// IE
window.onerror = function (message, file, line, column) {
var column = column || (window.event && window.event.errorCharacter);
var stack = [];
var f = arguments.callee.caller;
while (f) {
stack.push(f.name);
f = f.caller;
}
console.log(message, "from", stack);
}
I’ve updated Bugsnag’s JavaScript error monitoring to include all of these techniques, but there are still things I wish we could improve on.
If you’ve got ideas to fix these please send pull requests in exchange for fame, glory, and Bugsnag credit :)