Promises, Promises (or how not to lose your marbles)

Sometimes asynchronous programming can become overwhelming

In the days of synchronous languages, getting a record, updating it and writing it back (assuming an object orientated approach) would look something like this:

my $customer = Customer->load($id);
customer->last_called(DateTime->now);
customer->save;

That was fine because we were allowed to occupy the whole thread or process and nothing could happen in between.

In the days of asynchronous processing, it’s not the same. We now start off the job and tell the system to let us know, via some kind of callback, when it’s ready. Although we live in an asynchronous world, many programmers, new to async, have problems grasping this concept.

Breakfast

In everyday life, we have no issues with asynchronous paradigms. I put the kettle on, put the toast in the toaster and start to fry the eggs. The toaster calls me back by popping up so I move on to buttering the toast, meanwhile the eggs are ready and, while I’m dealing with them, the kettle boils, another callback. I leave the kettle callback until I’m finished with the eggs. No problems in an async world although sometimes things seem to be happening all too fast.

In Node.js, there are three basic concepts for dealing with callbacks: callback functions, events and promises.

Callbacks

Customer.load(id, function(err, customer) {
if (err) throw err;
customer.lastCalled = new Date();
customer.save(function(err, result) {
if (err) throw err;
// Result can be anything, customer still in scope
});
});

As you can see, and probably already know, this can get messy fast; we’re already at three levels of indentation and, if we need to do more processing, we’ll need several more.

Events

Customer.load(id)
.on(‘error’, function(err) {
throw err;
})
.on(‘loaded’, function(customer) {
customer.lastCalled = new Date();
customer.save()
.on(‘error’, function(err) {
throw err;
})
.on(‘saved’, function(result) {
// Result again can be anything, customer is in scope
});
});

Well, this looked like it was going to get simpler but it didn’t. We still needed three levels of indent and we still had to handle errors in two different places. Beyond that, we had to trap the right event, ‘loaded’ or ‘saved’, depending on which event name our called code decided to emit.

Promises

Customer.load(id)
.then(function(customer) {
customer.lastCalled = new Date();
return customer.save();
})
.then(function(customer) {
// Here we probably want our customer back in scope
})
.catch(function(err) {
// Process all errors here
});

Much neater. Only one level of indent and only one place to handle errors. The downside is that now we don’t have both the customer and a result in scope. This, though, is easily worked around but mostly there’s no work-around necessary.

So that is the advantage of using Promises but what about writing the code to generate the Promise? For Customer.load(), it might look something like this:

Customer.load = function(id) {
return new Promise(function(resolve, reject) {
// Call the database engine, getting a record via callback
getRecord(db, id, function(err, result {
if (err) {
reject(err);
}
else {
// process the result
if (problem) {
reject(new Error());
}
else {
resolve(result);
}
}
});
});
};

When writing Promise code, you are in one of two places. Either you are inside a new Promise() function or you are inside a .then(). Look how much simpler our code becomes if the database call also returns a Promise.

Customer.load = function(id) {
return Promise.resolve()
.then(function() {
return getRecord(db, id);
})
.then(function(result) {
// process the result
if (problem) {
throw new Error();
}
return result;
});
}

Now there’s no error handling needed as it can all be passed back to the .catch() in the calling code. Whether the error occurs in getRecord() or we throw it, the result is the same: it will be caught by the caller’s .catch().

What are those two places? When you’re in a new Promise() function, you need to be careful. You must end your processing either on resolve(result) to return a value or on reject(error) to return an error but not both. You need to be sure that both cannot get called and that each can only be called once. You can’t throw errors or you’ll probably break the whole program.

On the other hand, being in a .then() function is much easier. You just return your result or throw your error. You can have multiple returns and throws as clearly only the first will ever be reached.

And how do we make life easier for ourselves? Simply start the function with:

return Promise.resolve().then(function() {

Now we can chain other .then() functions, knowing that our errors will be propagated back to the caller and our last return value will be the result that the caller sees.

.then() function can finish on one of three things:

  • it can return synchronous value (an object, number, string etc.)
  • it can return another Promise
  • it can throw a synchronous error

If you return a synchronous value, it will actually convert that into a new Promise so that another .then() or .catch() can be added. The value returned will be passed into the next then(). If you throw an error, all the following .then()s will be skipped and it will be caught by the next .catch().

Mastering Promises is well worth the effort and there are lots of good resources to help you do so. If asynchronous programming is hard, Promises, used correctly, can make it easier. But they’re not the answer to everything. Sometimes a callback function can make sense or, if your code can return at multiple different times, events may be the way to go.

When I was still struggling with the concepts, I found this article particularly helpful.

Finally, if you’re wondering about the astonishing images of the Wintergatan Music Machine, you should watch this video. It’s amazing.