A Guide to Proper Error Handling in JavaScript
Save article ToRead Archive Delete · Log in Log out
11 min read · View original · sitepoint.com
This article was peer reviewed by Tim Severien and Moritz Kröger. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!
Ah, the perils of error handling in JavaScript. If you believe Murphyʼs law, anything that can go wrong, will go wrong! In this article I would like to explore error handling in JavaScript. I will cover pitfalls and good practices. We’ll finish by looking at asynchronous code and Ajax.
More from this author
I feel JavaScriptʼs event-driven paradigm adds richness to the language. I like to imagine the browser as this event-driven machine, and errors are no different. When an error occurs, an event gets thrown at some point. In theory, one could argue errors are simple events in JavaScript. If this sounds foreign to you, buckle up as you are in for quite a ride. For this article, I will focus only on client-side JavaScript.
This write up will build on concepts explained in the article Exceptional Exception Handling in JavaScript. To paraphrase: “with an exception JavaScript checks for exception handling up the call stack.” I recommend reading up on the basics if you are not familiar. My goal is to explore beyond the bare necessities for handling exceptions. The next time you see a nice try...catch
block, it will make you think twice.
The Demo
The demo we’ll be using for this article is available on GitHub, and presents a page like this:
All buttons detonate a “bomb” when clicked. This bomb simulates an exception that gets thrown as a TypeError
. Below is the definition of such a module with unit test.
function error() {
var foo = {};
return foo.bar();
}
To begin, this function declares an empty empty object named foo
. Note that bar()
does not get a definition anywhere. Let’s verify that this will detonate a bomb with a nice unit test.
it('throws a TypeError', function () {
should.throws(target, TypeError);
});
This unit test is written in Mocha with test assertions in Should.js. Mocha is a test runner while should.js is the assertion library. Feel free to explore these test APIs if you are not already familiar. A test begins with it('description')
and ends with a pass / fail in should
. The good news is unit tests run on node and do not need a browser. I recommend paying attention to the tests as they prove out key concepts in plain JavaScript.
As shown, error()
defines an empty object then tries to access a method. Because bar()
does not exist within the object it throws an exception. Believe me, with a dynamic language like JavaScript this can happen to anybody!
The Bad
On to some bad error handling. I have abstracted the handler on the button from the implementation. Here is what the handler looks like with unit tests:
function badHandler(fn) {
try {
return fn();
} catch (e) { }
return null;
}
This handler receives a fn
callback as a dependency. This dependency then gets called inside the handler function. The unit tests show how it is used.
it('returns a value without errors', function() {
var fn = function() {
return 1;
};
var result = target(fn);
result.should.equal(1);
});
it('returns a null with errors', function() {
var fn = function() {
throw Error('random error');
};
var result = target(fn);
should(result).equal(null);
});
As you can see, this wicked handler returns null
if something goes wrong. The callback fn()
can point to a legit method or a bomb. The click handler below tells the rest of the story.
(function (handler, bomb) {
var badButton = document.getElementById('bad');
if (badButton) {
badButton.addEventListener('click', function () {
handler(bomb);
console.log('Imagine, getting promoted for hiding mistakes');
});
}
}(badHandler, error));
What stinks is I just get a null
. This leaves me blind when I try to figure out what went wrong. This fail-silent strategy can range from bad UX all the way down to data corruption. What is frustrating with this is I can spend hours debugging the symptom but miss the try-catch block. This wicked handler swallows mistakes in the code and pretends all is well. This may go down well with organizations that donʼt sweat code quality. But, hiding mistakes will find you debugging for hours in the future. In a multi-layered solution with deep call stacks, it is impossible to figure out where it went wrong. There may be a few cases where doing a silent try-catch is legit. But as far as error handling, this is just bad.
A fail-silent strategy will leave you pining for better error handling. JavaScript offers a more elegant way of dealing with these types of issues.
The Ugly
Moving on, time to investigate an ugly handler. I will skip the part that gets tight-coupled to the DOM. There is no difference here from the bad handler we just saw. What matters is the way it handles exceptions as shown below with unit test.
function uglyHandler(fn) {
try {
return fn();
} catch (e) {
throw Error('a new error');
}
}
it('returns a new error with errors', function () {
var fn = function () {
throw new TypeError('type error');
};
should.throws(function () {
target(fn);
}, Error);
});
A definite improvement over the bad handler. Here the exception gets bubbled up the call stack. What I like is now errors will unwind the stack which is super helpful in debugging. With an exception, the interpreter will travel up the stack looking for another handler. This opens many opportunities to deal with errors at the top of the call stack. Unfortunately, since it is an ugly handler I lose the original error. So I am forced to traverse back down the stack to figure out the original exception. But at least I know something went wrong, which is the point of throwing an exception.
The ugly error handler is not as harmful but leads to code smell. Letʼs see if the browser has something up its sleeve to deal with this.
Unwind that Stack
So, one way to unwind exceptions is to place a try...catch
at the top of the call stack. Say:
function main(bomb) {
try {
bomb();
} catch (e) {
// Handle all the error things
}
}
But, remember I said that the browser is event-driven? Yes, an exception in JavaScript is no more than an event. The interpreter halts execution in the current executing context and unwinds. Turns out, there is an onerror
global event handler we can leverage. And it goes something like this:
window.addEventListener('error', function (e) {
var error = e.error;
console.log(error);
});
This event handler catches errors within any executing context. Error events get fired from various targets for any kind of error. What is so radical is this event handler centralizes error handling in the code. Just like with any other event, you can daisy chain handlers to handle specific errors. This allows error handlers to have a single purpose, if you follow SOLID principles. These handlers can get registered at any time. The interpreter will cycle through as many handlers as it needs to. The code base gets freed from try...catch
blocks that get peppered all over which makes it easy to debug. The key is to treat error handling like event handling in JavaScript.
Now that there is a way to unwind the stack with global handlers, what can we do with that? After all, may the call stack be with you.
Capture the Stack
The call stack is super helpful in troubleshooting issues. The good news is that the browser provides this information out of the box. Granted, the stack property in the error object is not part of the standard yet, but it is consistently available in the latest browsers.
So, one of the cool things we can do with this is log it to the server:
window.addEventListener('error', function (e) {
var stack = e.error.stack;
var message = e.error.toString();
if (stack) {
message += '\n' + stack;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', '/log', true);
xhr.send(message);
});
It may not be obvious from the code sample, but this event handler will fire along side the one I just showed above. As mentioned, every handler gets a single purpose which keeps the code DRY. What I like is how these messages get captured on the server.
Here is a screen shot of what this looks like in node:
This message comes from Firefox Developer Edition 46. With a proper error handler, note that it is crystal clear what the issue is. No need to hide mistakes here! Just by glancing at this, I can see what threw the exception and where. This level of transparency is awesome for debugging front-end code. These messages can get stored in persistent storage for later retrieval, giving further insight on what conditions trigger which errors.
The call stack is super helpful for debugging. Never underestimate the power of the call stack.
Async Handling
Ah, the perils of asynchrony! JavaScript rips asynchronous code out of the current executing context. This means try...catch
statements such as the one below have a problem.
function asyncHandler(fn) {
try {
setTimeout(function () {
fn();
}, 1);
} catch (e) { }
}
The unit test tells the rest of the story:
it('does not catch exceptions with errors', function () {
var fn = function () {
throw new TypeError('type error');
};
failedPromise(function() {
target(fn);
}).should.be.rejectedWith(TypeError);
});
function failedPromise(fn) {
return new Promise(function(resolve, reject) {
reject(fn);
});
}
I had to wrap the handler around a promise to verify the exception. Note that an unhandled exception occurs, although I have the code block around a nice try...catch
. Yes, try...catch
statements only work within a single executing context. By the time an exception gets thrown, the interpreter has moved away from the try-catch. This same behavior occurs with Ajax calls too. So, there are two options. One alternative is to catch exceptions inside the asynchronous callback:
setTimeout(function () {
try {
fn();
} catch (e) {
// Handle this async error
}
}, 1);
This approach will work, but it leaves much room for improvement. First of all, try...catch
blocks get tangled up all over the place. In fact, the 1970s programming called and they want their code back. Plus, the V8 engine discourages the use of try…catch blocks inside functions (V8 is the JavaScript engine used in the Chrome browser and Node). Their recommendation is to write those blocks at the top of the call stack.
So, where does this lead us? There is a reason I said global error handlers operate within any executing context. If you add an error handler to the window object, that’s it, you are done! Isn’t it nice that the decision to stay DRY and SOLID is paying off? A global error handler will keep your code nice and clean.
Below is what this exception handler reports on the server. Note that if you’re following along with the demo code, the output you see may be slightly different depending on which browser you’re using.
This handler even tells me that the error is coming from asynchronous code. It tells me it is coming from a setTimeout()
handler. Too cool!
Conclusion
In the world of error handling there are at least two approaches. One is the fail-silent approach where you ignore errors in the code. The other is the fail-fast and unwind approach where errors stop the world and rewind. I think it is clear which of the two I am in favor of and why. My take: don’t hide problems. No one will shame you for accidents that may occur in the program. It is acceptable to stop, rewind and give users another try. In a world that is far from perfect, it is important to allow for a second chance. Errors are inevitable, it’s what you do about them that counts.