Why promises need to be done

Use .done() at the end of your Q promise chains to throw any unhandled exception.

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo(cb) {
throw new Error('problem');
setTimeout(function () {
cb(1);
}, 10);
}
try {
foo(function (value) {
console.log('foo finished with value', value);
});
} catch (err) {
console.error('caught', err);
}
// output
$ node index.js
caught [Error: problem]

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
2
3
4
5
6
7
8
9
10
11
12
13
function foo(cb) {
setTimeout(function () {
cb(1);
}, 10);
}
try {
foo(function (value) {
throw new Error('problem');
console.log('foo finished with value', value);
});
} catch (err) {
console.error('caught', err);
}
output
1
2
3
4
5
6
7
$ node index.js
/index.js:22
throw new Error('problem');
^
Error: problem
at null._onTimeout (/index.js:22:11)
at Timer.listOnTimeout (timers.js:124:15)

This is because the runtime execution looks different from the static source. Instead it looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
function foo(cb) {
setTimeout(function () {
cb(1);
}, 10);
}
try {
foo(function (value) {
goto callback:
});
} catch (err) {
console.error('caught', err);
}
1
2
3
callback:
throw new Error('problem');
console.log('foo finished with value', value);

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
2
3
4
5
6
7
8
9
10
11
12
13
var q = require('q');
function bar() {
var defer = q.defer();
setTimeout(function () {
defer.reject(new Error('problem'));
}, 10);
return defer.promise;
}
bar().then(function (value) {
console.log('bar resolved with value', value);
}).fail(function (err) {
console.error('caught', err.message);
});
output
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
2
3
4
5
var promises = [];
promises.push(bar());
promises = promises.map(function (p) {
return p.fail(console.error);
});

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
2
3
4
5
6
7
8
9
10
11
12
13
var q = require('q');
function bar() {
var defer = q.defer();
setTimeout(function () {
defer.reject(new Error('problem'));
}, 10);
return defer.promise;
}
bar().then(function (value) {
console.log('bar resolved with value', value);
});
// prints nothing!
$ node index.js

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
2
3
4
bar().then(function (value) {
console.log('bar resolved with value', value);
}).done();
// prints exception

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var q = require('q');
function bar() {
throw new Error('problem');
var defer = q.defer();
setTimeout(function () {
defer.resolve(1);
}, 10);
return defer.promise;
}
bar().then(function (value) {
console.log('bar resolved with value', value);
}).fail(function (err) {
console.error('Caught', err.message);
}).done();
// output
$ node index.js
/index.js:4
throw new Error('problem');
^
Error: problem
at bar (/index.js:4:9)

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
2
3
4
5
6
7
8
9
// same setup as above
q.fcall(bar).then(function (value) {
console.log('bar resolved with value', value);
}).fail(function (err) {
console.error('Caught', err.message);
}).done();
// output
$ node index.js
Caught problem

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
2
3
4
5
6
7
8
9
10
q('foo').then(function (value) {
console.log('resolved with value', value);
throw new Error('Went wrong in .then');
}).fail(function (err) {
console.error('Caught', err.message);
throw new Error('.fail crashed!');
});
// output
resolved with value foo
Caught Went wrong in .then

Adding .done

1
2
3
4
5
6
7
8
9
10
11
12
q('foo').then(function (value) {
console.log('resolved with value', value);
throw new Error('Went wrong in .then');
}).fail(function (err) {
console.error('Caught', err.message);
throw new Error('.fail crashed!');
}).done();
// output
resolved with value foo
Caught Went wrong in .then
Error: .fail crashed!
at /index.js:38:9

Second, .done guarantees that no one can attach to this promise chain.

1
2
3
4
5
6
7
8
9
var chain = q('foo').then(function (value) {
console.log('resolved with value', value);
throw new Error('Went wrong in .then');
}).fail(function (err) {
console.error('Caught', err.message);
throw new Error('.fail crashed!');
});
// somewhere down the line
chain.then(...);

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