Error handling in JavaScript: a better way

Let’s not avoid the elephant in the room: error handling in JavaScript is quite abysmal. I’ll try to outline what is wrong with it and present a better way.

The largest problem by far is that it’s hard to find a good example on how to do error handling in JavaScript. If you search for it you’ll probably find something to do with the window.onerror event or a script called stacktrace.js.

window.onerror

onerror is an event on the global object (window). Any uncaught error will appear as an onerror event. onerror has some very large problems:

  • onerror doesn’t give you much information about the error. It gives you the message, the file name and the line number. It doesn’t give you a stack, it doesn’t give you any kind of context
  • onerror triggers for all JavaScript errors occuring in that window. If a visitor of your site uses a browser plugin that runs some JavaScript, which throws an error, you will get that error in onerror. If you’re using a 3rd party script, such as an analytics tool or an ad supplier, you will get their uncaught errors too. There are of course ways to filter these errors but it’s not a very robust way of dealing with it.

These are strong indications that onerror is not the tool you should be using.

stacktrace.js

stacktrace.js takes a different approach to error handling. It doesn’t use onerror. Instead it uses the try-catch language feature. It tries to build a stacktrace by using a very crude form of reflection. It walks the caller/callee stack and toStrings functions to find their name and signature.

Again, this is a strong indication that we’re not using the right tool.


So what is the right tool? What are we missing?

We’re missing proper errors. “Exceptions” as in Java and PHP are pretty good, can we have those? Yes, actually, we can. JavaScript has the Error class. Let me explain what it is and why you should use it.

In JavaScript you can throw anything, since everything is an Object and there is no native concept like Java’s “Throwable” interface. That’s not necessarily a good thing. This is one of those cases where too much freedom can be a bad thing. I don’t necessarily believe JavaScript should enforce it, but you should at least enforce it yourself. My recommendation is: always throw things that are Errors, or something that inherits from it.

throw "Unexpected input"; // bad
throw new Error("Unexpected input"); // good

Inheritance in JavaScript is terribly messy so use a framework and get it out of the way. Inheriting from the Error class is a good idea if you want to add more information to your errors, or if you want to distinguish between different types. Example:

var ContextError = extend(Error, function (message, context) {
    this.message = message;
    this.context = context;
})
ContextError.prototype.getContext = function () {
    return this.context;
};

try {
    try {
        throw new ContextError("foo", { bar: "baz" });
    } catch (e) {
        if (e instanceof ContextError) {
            console.log(e.message, e.getContext());
        } else {
            throw e; // rethrow
        }
    }
} catch (e) {
    console.log(e.message);
}

It’s not rocket science. This is what you should be doing.

Another massive advantage of using Errors is that browser run-times are starting to augment it with all the tools we want. Errors have a stack property in modern browsers – I’ve confirmed it exists in Chrome, Firefox and IE10. This is a huge boon. Generating a readable stacktrace from the stack property is fairly trivial. I expect support for the Error class to grow in the future.

Chrome/v8

Chrome’s v8 engine goes one step further and tries to give you even better stacktraces. Error.prepareStackTrace allows you specify what the Error.stack property will look like.

Error.prepareStackTrace = function (error, stack) {
	return stack;
};

This gives you access to the full StackFrames that v8 supplies. http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi

By default Chrome limits the length of the stacktrace to 10. You can increase the limit by:

Error.stackTraceLimit = 50;

There is one more thing I want to share.

If you’re using asynchronous functions and events (which you should) then you’ll notice that errors thrown in other events aren’t caught by your try-catch and if you do catch them your stacktraces are cut-off at the start of each event. This is a limitation in the way the try-catch works, it doesn’t automatically work with a language that has an event-loop. If do the following:

try {
    document.getElementById("a_link").onclick = function () {
        throw new Error("catch me if you can (you can't)");
    }
} catch (e) {
    console.log(e); // :<
}

The error thrown in the click event will not be caught by that try-catch statement. This is because it occurs in a different event.

My recommendation: wrap all event handlers in your own try-catch error handler. I’ve found there are actually only 3 places where new events happen: setTimout, setInterval and addEventListener.

function handle_error(error) {
    console.log("Gotcha: ", error.message);
}
function on (eventname, object, handler) {
    object.addEventListener(eventname, function (event) {
        try {
            handler(event);
        } catch (e) {
            handle_error(e);
        }
    });
}
on("click", document.getElementById("a_link"), function () {
    throw new Error("catch me if you can");
}

This allows you to catch all errors, but what about getting the stack from the parent event?

I have a solution for this, but it’s not a perfect solution. It leaks memory. The more events you register and the ‘deeper’ you go the more memory it needs. It does work though and luckily in most cases the memory requirement doesn’t cause any issues. That is the disclaimer, here is the code:

var wrap_try_catch = function () {
	var exception_stack = [];
	return function (func) {
		var live_exception_stack = exception_stack.concat([]);    // clone context_stack
		live_exception_stack.push(new Error("Capturing context before asynchronous call"));
		return function try_catch() {
			exception_stack = live_exception_stack;
			try {
				return func.apply(this, arguments);
			} catch (e) {
				var exception;
				if (typeof e === "string") {
					exception = new Error("String thrown: `" + e + "` throw a 'new Error' to get a better stacktrace");
				} else {
					exception = e;
				}
				handle_exception(exception_stack.concat([ exception ]));
				throw e;
			}
		};
	};
}());

// Usage:

function on(eventname, object, handler) {
    object.addEventListener(event_type, wrap_try_catch(handler));
}

wrap_try_catch makes an Error when you call it so you have access to a stack up to that point. For each new event it adds another Error on to the stack. When an Error occurs it combines that Error with the stack of Errors it has collected. This allows you to get a full stacktrace. Again, this leaks memory of course because it has to keep track of all these Error objects. It’s also not super fast as making an Error object is non-trivial (it takes some time). I’ve found no other/faster way to get a stack.

Leave a comment