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 contextonerror
triggers for all JavaScript errors occuring in thatwindow
. If a visitor of your site uses a browser plugin that runs some JavaScript, which throws an error, you will get that error inonerror
. 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 toString
s 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 Error
s, 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 Error
s is that browser run-times are starting to augment it with all the tools we want. Error
s 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 Error
s 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.