Error monitoring in JavaScript is a thorny problem. On the one hand
window.onerror
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. I've updated the Bugsnag notifier using the techniques below, ordered by effectiveness.
Modern Chrome and Opera (i.e. anything based around the
Blink rendering engine) fully support the HTML
5 draft spec for
ErrorEvent
and
window.onerror
.
In both of these browsers you can either use window.onerror
, or (amazingly!)
bind to the 'error' event properly:
// 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 window.onerror
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 try{ }catch(e){
}
block and then look at e.stack
. 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.
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
addEventListener
,
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 addEventListener
so that it
automatically wraps the callback we can automatically insert try{ }catch(e){
}
around most code. This lets existing code continue to work, but adds
high-quality exception tracking.
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 removeEventListener
keeps working. At the
moment it won't because the argument to addEventListener
is changed. Again
we only need to fix this on the prototype object:
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 setTimeout
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 <9, window.onerror
is called with the function call stack intact. This
means we can use arguments.callee.caller
recursively to build up a fake
stacktrace. It only gives us function names, not line numbers, but it's much
better than nothing.
// IE <9
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 the Bugsnag JavaScript notifier to include all of these techniques, but there are still things I wish we could improve on.
Stacktraces in IE 9. window.onerror
was called with the call stack intact
in IE 6,7 and 8; and exceptions have .stack
properties in IE 10 and 11.
This means that IE 9 is in limbo, you can't get a stacktrace using any of the
above techniques.
Column numbers in Firefox. Firefox is the only browser that doesn't support column numbers for exceptions. This makes debugging minified javascript essentially impossible. I've sent one patch but there's a lot more to do.
Other async entry points. We've hooked into setTimeout
, setInterval
and
requestAnimationFrame
. I think there are a few other candidates for
wrapping automatically, but I can't think of a reliable way to find them all.
If you've got ideas to fix these please send pull requests in exchange for fame, glory, and Bugsnag credit :).
Bugsnag captures exceptions in real-time from your web, mobile and desktop applications, helping you to understand and resolve them as fast as possible.
You should create a free Bugsnag account today.