Starting promises

Good way to call the first promise-returning function.

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
2
3
4
5
6
function delay(n) {
var Q = require('q');
var defer = Q.defer();
setTimeout(defer.resolve, n);
return defer.promise;
}

Let us use delay method to wait for 1 second before printing a message "finished".

1
2
3
4
5
6
delay(1000)
.then(function finished() {
console.log('finished');
})
.done();
// prints "finished" after 1 second

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
2
3
4
5
6
7
8
delay(1000)
.then(function () {
console.log('finished');
throw new Error('problem');
})
.done();
// prints "finished" after 1 second
// Error: problem

We can handle the exception down the promise chain. For example using catch() callback

1
2
3
4
5
6
7
8
9
10
11
12
delay(1000)
.then(function finished() {
console.log('finished');
throw new Error('problem');
})
.catch(function (err) {
console.log('Caught a problem', err.message);
})
.done();
// prints
// finished
// Caught a problem problem

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function delay(n) {
if (n < 0) {
throw new Error('invalid delay ' + n);
}
var Q = require('q');
var defer = Q.defer();
setTimeout(defer.resolve, n);
return defer.promise;
}
delay(-1000)
.then(function () {
console.log('finished');
})
.done();
// immediately prints
// Error: invalid delay -1000

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var n = -1000;
var chain;
try {
chain = delay(n);
} catch (err) {
console.log('using default delay because', err.message);
chain = delay(1000);
}
chain.then(function () {
console.log('finished');
})
.done();
// prints
// using default delay because invalid delay -1000
// finished

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function delay(n) {
var Q = require('q');
var defer = Q.defer();
if (n < 0) {
defer.reject('invalid delay ' + n);
} else {
setTimeout(defer.resolve, n);
}
return defer.promise;
}
delay(-1000)
.then(function () {
console.log('finished');
}, function (err) {
console.log('problem delaying', err);
})
.done();
// prints
// problem delaying invalid delay -1000

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function delay(N) {
var Q = require('q');
var defer = Q.defer();
if (n < 0) {
defer.reject('invalid delay ' + n);
} else {
setTimeout(defer.resolve, n);
}
return defer.promise;
}
delay(-1000)
.then(function () {
console.log('finished');
}, function (err) {
console.log('problem delaying', err);
})
.done();
/*
prints exception
if (n < 0) {
ReferenceError: n is not defined
*/

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Q = require('q');
function delay(N) {
var defer = Q.defer();
if (n < 0) {
defer.reject('invalid delay ' + n);
} else {
setTimeout(defer.resolve, n);
}
return defer.promise;
}
Q.try(delay, -1000)
.then(function () {
console.log('finished');
}, function (err) {
console.log('problem delaying', err.message);
})
.done();
// prints
// problem delaying n is not defined

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Q = require('q');
function delay(N) {
var defer = Q.defer();
setTimeout(defer.resolve, n);
return defer.promise;
}
Q()
.then(delay.bind(null, 1000))
.then(function finished() {
console.log('finished');
throw new Error('problem');
})
.catch(function (err) {
console.log('Caught a problem', err.message);
})
.done();
// prints
// Caught a problem n is not defined

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
2
3
4
5
6
7
8
9
10
11
12
13
14
function finished() {
console.log('finished');
throw new Error('problem');
}
function problem(err) {
console.log('Caught a problem', err.message);
}
Q(1000)
.then(delay)
.then(finished)
.catch(problem)
.done();
// prints
// Caught a problem n is not defined

This is as simple as it gets - but the entire chain runs asynchronously!

Related: Why promises need to be done