JavaScript promises are the best way I know for working with asynchronous logic.
I have described every aspect of using promises in my blog posts, except
how to start them correctly. The most important aspect when starting the promise chain
is how to handle the unexpected errors. To start, let us review how we handle the errors
that happen inside .then()
callbacks
Handling errors in the middle of the promise chain
Imagine we have a function that returns a promise, for example to delay the execution by N milliseconds.
1 | function delay(n) { |
Let us use delay
method to wait for 1 second before printing a message "finished".
1 | delay(1000) |
What happens if there is an exception in the finished
function? Promise runtime catches this error and passes
it to the next step, hoping that there is an error handler registered somewhere down the chain. If no error
handler is found before reaching .done()
call, then an exception is thrown.
1 | delay(1000) |
We can handle the exception down the promise chain. For example using catch()
callback
1 | delay(1000) |
The promises offer a systematic way to handle any unexpected errors, but there is a missing piece: how to handle an error inside the very first promise-returning function that kicks of the chain. There are two approaches to kicking off the initial promise and handling the errors. First is to let error bubble synchronously. The second approach is to wrap the first function call using promise engine's methods.
Sync error, async result
Let us go back to the original function delay
. Let us validate the input delay argument n
.
It should be positive number of milliseconds.
Otherwise, the delay is invalid. We will throw an error right away (sync error), if n
is negative.
1 | function delay(n) { |
Throwing a sync error, instead of using defer.reject(...)
allows the caller to handle the error right away.
For example we could use user-entered delay value, or if delay
throws an error use default delay
1 | var n = -1000; |
Async error and result
I do not particularly like the above "sync error, async result". Mostly because this assumes that the caller
can actually do something about the error and restart the chain. If the caller could restart the chain
with correct parameters, it should have validated the parameters in the first place.
Often, the most the caller can do is pass the error back to the outside world, just like defere.reject(...)
or a thrown exception does.
If we expect an invalid input, we could reject the promise at the start, giving the following code a change to handle the rejection.
1 | function delay(n) { |
But what if the exception is truly unexpected? For example the same code, if we named the argument incorrectly does NOT handle the raised error.
1 | function delay(N) { |
If the ReferenceError
happened in the .then()
callback, it would have been handled by the promise engine,
maybe by the next error handler. Because it happens before the promise engine wraps the callback, the
error goes unhandled. This is why we can use a helper utility, usually provided by the promise library
to execute the given function and start the promise chain. In the case of my favorite Node library q,
there is Q.try. We just pass our function and its arguments to Q.try
and any runtime exceptions
inside our function will be passed along the promise chain.
1 | var Q = require('q'); |
The same ReferenceError
now will be handled by the error handler, unifying the first step of the chain with
the regular error or rejection handling.
AngularJS $q service
If you use AngularJS $q in the browser, you do not have Q.try
($q service is a small subset of the Q library). Fortunately, adding the feature equivalent to Q.try
is simple.
Ben Nadel shows one way in Monkey-Patching The $q Service Using $provide.decorator() In AngularJS.
I hope he releases it as a stand alone library on NPM / Bower.
Starting with "dummy" promise
Another way to start the promise and have the error handling is to start with a "dummy" promise.
In case of Q, you can do the following (delay
throws ReferenceError because I used wrong variable on purpose).
1 | var Q = require('q'); |
Because we shifted our promise-returning function to be inside .then
callback, we direct the error
to the .catch
handler.
The above example works even better if we resolve the dummy promise with the first argument needed
for the actual function delay
. We can also factor our the little callback functions.
Then the steps look very natural and eady to read.
1 | function finished() { |
This is as simple as it gets - but the entire chain runs asynchronously!
Related: Why promises need to be done