Working with JavaScript requires mastering asynchronous code. The two most common patterns for doing so are callbacks and promises. Promises are great because they offer us more flexibility: if a function gets a promise from another function it can either attach callbacks to it or just pass the callback to the caller.
Therein, however, lies the danger of forgetting to do either, which can lead to hard-to-track bugs. For that reason, the #1 thing to remember when working with JavaScript promises is:
When you get a promise from another function, ALWAYS attach an error handler at the end of the chain, UNLESS you are going to return the final promise to the caller.
If you do not, then any errors in the code are going to be silently swallowed, which makes debugging a nightmare. The only time you can skip an error
handler is when you are passing the promise (and the responsibility) to the caller.
Let’s look at a few examples based on a hypothetical getPromise() function that returns a promise, and a synchronous process() function that does
something with the result of the promise:
getPromise()
.then(function(result) {
process(result);
});
This is REALLY BAD. Any errors either in getPromise() or in process() would disappear into thin air. Basically, what this code says is: “If something goes wrong, don’t bother me, because I really don’t care.”
So, let’s attach an error handler:
getPromise()
.then(function(result) {
process(result);
}, $log.error);
Here we are passing Angular’s $log.error() error logging function as the error handler. This is an improvement, but still rather poor. In fact, this code can be even more dangerous, since it may provide a false sense of security. In this sample, errors in getPromise() will get logged but errors in process() will still get swallowed. Why? Because we attached $log.error to the original promise only! Errors in process(), however, would lead to the rejection of the new promise returned by then(). That promise does not have any error handlers attached to it. And it should.
getPromise()
.then(function(result) {
return process(result);
})
.then(null, $log.error);
This is GOOD. All errors would be logged. Of course, simply logging an error is rarely the best choice - you should probably do something to notify the user. But even simply attaching $log.error as the error handler would make a huge difference in the ease of debugging. Also, notice that you do not need to attach an error handler to the earlier then’s, as long as you attach one at the very end.
An aside on the then(null, errorHandler) pattern: Different promise implementations provide for different functions for attaching the error callback: .catch(), .fail(), onError(), etc. Nearly all of them, however, allow you to attach an error handler with .then(null, errorHandler). Sticking with this pattern makes it easier to swap one library for another, e.g., in the context of testing.
Now, what if you are want to return a promise? This changes things.
return getPromise()
.then(function(result) {
return process(result);
});
This is OK. You are passing the responsibility for the error to your caller. If anything goes wrong either in getPromise() or in process(), your caller will get a rejected promise. This means, of course, that are trusting your caller to do the right thing. Hopefully, your caller is careful enough to handle the rejection. Sometimes it makes sense to catch some of the errors yourself. Sometimes it is better to send them to the caller.
What if we just log the error ourselves:
return getPromise()
.then(function(result) {
return process(result);
})
.then(null, $log.error);
This may be ok, as long as you understand what you are doing. You are catching all the errors yourself and sending your caller a promise that cleanly resolves to “undefined.” Your caller won’t know there was an error. They may wonder why the result is “undefined.”
We do not have to choose between those to approaches, however. We can do both:
return getPromise()
.then(function(result) {
return process(result);
})
.then(null, function(error) {
$log.error(error);
throw error;
});
Here we are logging the error and then re-throwing it. The caller will get a rejected promise, much as if we hadn’t attached any handlers to it, but the error will have been logged. This is the safest, most defensive approach. It may even be worth wrapping this handler in a named function to make it reusable:
function logAndRethrow(error) {
$log.error(error);
throw error;
}
. . .
return getPromise()
.then(function(result) {
return process(result);
})
.then(null, logAndRethrow);
Happy hacking!