Asynchronous error handling in JavaScript
Dealing with errors can be especially tricky in asynchronous situations.
Anything that can go wrong will go wrong, so we better prepare ourselves. The lessons we’ve been taught as programmers to nicely throw and catch exceptions don’t apply anymore in asynchronous environments. Yet asynchronous programming is on the rise, and things still can and therefore will go wrong. So what are your options to defend against errors and graciously inform the user when things didn’t go as expected? This post compares different asynchronous error handling tactics for JavaScript.
The information in this blog post is outdated. Like all aspects of JavaScript, error handling has changed significantly since 2012. Look at async
/await
with regular try
/catch
statements for modern asynchronous error handling. The original text and discussion are below for archival purposes.
The easy case is when actions happen synchronously. Suppose you want to post a letter to a friend. Synchronous behavior is when you follow each step of the process and wait. So you pick up the envelope, put it in your pocket, ride with your bike to your friend’s place, and deposit it in her letter box. If something goes wrong, such as you losing the envelope along the way, then you react as it happens by ringing the doorbell and apologizing.
An asynchronous way to do the same thing would be to call postal services. You hand over the letter to them, and they will do the steps for you, while you do something else. However, how will you know if things go wrong? After all, you don’t witness the envelope sliding into the mailbox. Will the mailman call you on success or failure? Or will you call your friend to confirm successful arrival?
Synchronous error handling
On of the earliest techniques that predates exceptions was to verify success depending on a function’s return value.
function postLetter(letter, address) {
if (canSendTo(address)) {
letter.sendTo(address);
return true;
}
return false;
}
You might have encountered this in C code. The caller thus inspects the value:
if (postLetter(myLetter, myAddress))
console.log("Letter sent.");
else
console.error("Letter not sent.");
However, this is not convenient if the function also has to return an actual value. In that case, the caller would have to check whether the return value is legitimate or an error code. For that reason, exceptions have been invented.
function postLetter(letter, address) {
if (canSendTo(address)) {
letter.sendTo(address);
return letter.getTrackingCode();
}
throw "Cannot reach address " + address;
}
The caller can then nicely catch the exception in a dedicated place.
try {
var trackingCode = postLetter(myLetter, myAddress);
console.log("Letter sent with code " + trackingCode);
}
catch (errorMessage) {
console.error("Letter not sent: " + errorMessage);
}
Unfortunately, exceptions are only a synchronous mechanism, which is logical: in an asynchronous environment, the exception could be thrown when the handler block is already out of scope and thus meaningless.
Error callbacks
One of the characteristics of asynchronous functions is that they cannot immediately calculate their return value (otherwise, they wouldn’t be asynchronous). Instead, the return value is passed through a callback function. The same mechanism can pass an error value. One option is to use a separate error callback.
function postLetter(letter, address, onSuccess, onFailure) {
if (canSendTo(address))
letter.sendTo(address, function () {
onSuccess(letter.getTrackingCode());
});
else
onFailure("Cannot reach address " + address);
}
Note that sendTo
is now also asynchronous here, calling the specified function after the letter has been send. The corresponding caller code looks like this:
postLetter(myLetter, myAddress,
function (trackingCode) {
console.log("Letter sent with code " + trackingCode);
},
function (errorMessage) {
console.error("Letter not sent: " + errorMessage);
});
It’s a bit more verbose, but that’s inherent to asynchronous programming with callbacks. An alternate solution is to stick to a single callback, adding an extra error argument. This is the approach many of the Node.js APIs take.
function postLetter(letter, address, callback) {
if (canSendTo(address))
letter.sendTo(address, function () {
callback(null, letter.getTrackingCode());
});
else
callback("Cannot reach address " + address);
}
If the first argument is non-null, an error has occurred. This can be seen in the caller:
postLetter(myLetter, myAddress,
function (errorMessage, trackingCode) {
if (errorMessage)
return console.error("Letter not sent: " + errorMessage);
console.log("Letter sent with code " + trackingCode);
});
Handling errors with promises
While callbacks provide a viable solution, they don’t let us benefit from the added temporal flexibility that asynchronous programming offers. What if, at a certain point in time, we want to change the performed action in case of success or failure? Furthermore, it’s a pity to ignore the provided functionality of return values. This is where promises come in. A promise is a placeholder value that can be returned by an asynchronous function. It acts as an event emitter that will indicate when the actual value is available, or when an error has occurred. Many implementations exist, such as Q, when.js and my own promiscuous. This example uses jQuery since many are familiar with this library (although its promise implementation is debated).
The idea is that the asynchronous function creates a deferred object that provides the necessary functionality. Then, the function will return the promise that is created by the deferred object. The function resolves the deferred in case of success, or rejects it in case of failure.
function postLetter(letter, address) {
var deferred = new $.Deferred();
if (canSendTo(address))
letter.sendTo(address, function () {
deferred.resolve(letter.getTrackingCode());
});
else
deferred.reject("Cannot reach address " + address);
return deferred.promise();
}
Note how the function’s signature is the same as the synchronous one. The return type, however, is different: it’s not a string but a promise. The caller can register success functions with done
and failure functions with fail
:
var trackingCodePromise = postLetter(myLetter, myAddress);
trackingCodePromise.done(function (trackingCode) {
console.log("Letter sent with code " + trackingCode);
});
trackingCodePromise.fail(function (errorMessage) {
console.log("Letter not sent: " + errorMessage);
});
Or, more compact and jQueryish:
$.when(postLetter(myLetter, myAddress))
.then(function (trackingCode) {
console.log("Letter sent with code " + trackingCode);
},
function (errorMessage) {
console.log("Letter not sent: " + errorMessage);
});
The fact that the latter code resembles the error callback code is no coincidence. Promises are in fact syntactic sugar. However, they can improve your code structure and readability, increasing the maintainability of your software.
Domain-bound exceptions
The above two techniques are the classical ways to handle asynchronous errors. Node 0.8 has introduced the experimental Domain API, which allows the combination of asynchrony and exceptions. In fact, domains are a mechanism that allows to define exception handlers as callbacks.
Thereby, domains offer a hybrid solution: the function sends its return value through a callback, but errors are thrown through exceptions.
function postLetter(letter, address, callback) {
if (!canSendTo(address))
throw "Cannot reach address " + address;
letter.sendTo(address, function () {
callback(letter.getTrackingCode());
});
}
The caller has to create a domain and register an error handler. Then, the calling code has to run inside the domain.
var postDomain = domain.create();
postDomain.on('error', function (errorMessage) {
console.log("Letter not sent: " + errorMessage);
});
postDomain.run(function () {
postLetter(myLetter, myAddress, function (trackingCode) {
console.log("Letter sent with code " + trackingCode);
});
});
Domains have other advantages and possibilities, such as implicit and explicit binding, but the details would take us too far here.
Pick what you need
In the end, you are free to choose the mechanism that works best for you. I recommend promises for browser-based code, as UI-driven development tends to depend a lot on callbacks and deferreds allow a clean separation between success and failure code. In JavaScript server code, I never felt the need for deferreds. I tend to go for the conventional Node.js callbacks, although they can automatically be converted into deferreds. We still have to see whether the domain approach finds adoption, although I tend to use asynchronous utility libraries instead, as they ease nested callbacks and associated error handling.
Try out the examples on GitHub to discover the various error handling strategies. What’s your favourite option—and do you know other techniques?