I always advise
to close chains of promises with .done()
call,
but never explained clearly why this is necessary. This is to tell
promise engine that no one is going to handle an exception and it
can be thrown out into the environment (potentially crashing the application).
Catching errors in async code
Error handling using try-catch
blocks in async code is hard. Let's take
a simple example that works as expected
1 | function foo(cb) { |
If the exception happens inside the original call, the surrounding try-catch
block
works. But if we move the exception into the callback function, the catch
block
no longer works.
1 | function foo(cb) { |
1 | $ node index.js |
This is because the runtime execution looks different from the static source. Instead it looks something like this:
1 | function foo(cb) { |
1 | callback: |
So the code executing the callback is outside the try-catch
lines, despite
what the source says.
Promises unify error catching
Let us look at error handling in the promises
1 | var q = require('q'); |
1 | caught problem |
Instead of throwing an error, we explicitly reject the deferred object (even using
try-catch
block). Notice we had an error handling .fail
callback attached to the
promise returned by bar()
function. This allows to define how to handle an exception
(or any rejected promise) dynamically. For example we could write:
1 | var promises = []; |
So we are completely free to handle errors in any way. Instead of the error handling
depending on the current execution stack (and the position of try-catch
blocks),
the errors are handled dynamically.
Except the promise itself has no idea if you plan to attach more steps, or if you plan to attach an error handler in the future. The promise just keeps the error inside waiting for someone to handle it.
1 | var q = require('q'); |
Thus the promise keeps the error, and if the program exits, or the entire promise chain is garbage collected because it is no longer referenced from anywhere, the promise holding the error has no idea what has happened.
This is why you use .done()
- you are telling the promise chain "there will be
no more steps, and no error handler". Thus any unhandled errors during the existing
steps can be thrown into the environment.
1 | bar().then(function (value) { |
Bonus 1
The default exception stack does not show the promise chain, for example the above code shows only
Error: problem
at null._onTimeout (/index.js:5:18)
at Timer.listOnTimeout (timers.js:124:15)
You can get much better stack by enabling
long stack traces in Q.
Same code as above but with extra line q.longStackSupport = true;
in the beginning prints the steps:
Error: problem
at null._onTimeout (/index.js:6:18)
at Timer.listOnTimeout (timers.js:124:15)
From previous event:
at Object.<anonymous> (/index.js:10:7)
at node.js:1031:3
Bonus 2
If an exception happens in the first promise-returning function, it is NOT handled by the promises, because you are still executing outside the promise handler
1 | var q = require('q'); |
Luckily, Q provides a wrapper that can call your function and catch the error directing it to the promise handler. It even support passing arguments to the function.
1 | // same setup as above |
Bonus 3
Do I need to use
.done
after.fail
?
Yes, you should close promise chain with .done
even if you have .fail
at the end, for 2 reasons:
First, what if there is crash in .fail
handler? Without .done
it will be silently swallowed.
1 | q('foo').then(function (value) { |
Adding .done
1 | q('foo').then(function (value) { |
Second, .done
guarantees that no one can attach to this promise chain.
1 | var chain = q('foo').then(function (value) { |
So I think it is good practice to close the chain if you don't want anyone attaching more actions.
Related: Starting promises
Bonus 4
io.js includes a handler that is called on every unhangled promise rejection, see process.html