JS stacktraces. The good, the bad, and the ugly.

@ConradIrwin

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.

The Good

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);
});

The Bad

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.

Automating

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 Ugly

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);
}

Still TODO

I've updated the Bugsnag JavaScript notifier to include all of these techniques, but there are still things I wish we could improve on.

  1. 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.

  2. 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.

  3. 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 :).

Create Your Bugsnag Account

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.