From errbacks to Promises in Node

In javascript world many APIs and libraries require you to implement most logic using callbacks. As of ES6, also referred to as ES2015, we can use the native Promise API. In most modern browsers and the current releases of Node.js the Promise API is supported. This enables us to use Promises without any thirdparty libraries.

In Node.js we all know most of the libraries or built-in components are using callbacks. Those callbacks all have the same method signature, also referred to as errbacks. An errback always has an error parameter first and subsequent parameters can be used to pass any data your API is expected to return. These callbacks are the main reason why Node.js and javascript in general have their asynchronous behaviour. Below an example of an errback function used with the mongoose connect function.

1
2
3
4
5
6
7
8
mongoose.connect('mongodb://localhost/my-db', function (err, db) {
if (err) {
console.error(`Unable to connect to database, Error: ${err}`);
} else {
console.log('Succesfully connected to database');
// here we can do anything with the db parameter.
}
});

The callback approach

As we all know callbacks will usually bring you into the situation where your code is going to be nested more and more. Also referred to as callback hell. See below an example of this pyramid kind piece of code. Also imagine we have even more method calls in there with callbacks, can you picture how big the pyramid will get?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function validateToken(username, token, callback) {
openDatabase(function (db) {
getCollection(db, 'user_tokens', function (err, collection) {
if (err) {
callback(err);
} else {
find(collection, { user: username }, function (err, result) {
if (err) {
callback(err);
}
callback(null, result.token === token);
});
}
});
})
}
validateToken('marco', 'abc123def456', function (err, valid) {
if (err) {
console.error('Sigh, something went wrong! Error: ' + err);
} else {
if (valid) {
console.log('awesome your token is valid!');
} else {
console.log('Nononoooo, that token is invalid!');
}
}
});

We can avoid this kind of code, utilizing Node.js module capabilities and named methods, as described in this article. I won’t cover this further in this blogpost as that is out of the scope. Instead I am going to show you how we can utilize Promises to improve the above code.

With the arrival of Promises we can write code as following. So imagine the openDatabase method from earlier example returns us a Promise. This will allow us to call that method as following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
validateToken(user, token) {
return openDatabase()
.then(function (db) {
return getCollection(db, 'user_tokens');
}).then(function (collection) {
return find(collection, { user: username });
}).then(function (result) {
return result.token === token;
});
});
validateToken('marco', 'abc')
.then(function (valid) {
if (valid) {
console.log('awesome your token is valid!');
} else {
console.log('Nononoooo, that token is invalid!');
}
})
.catch(function (error) {
console.error('Sigh, something went wrong! Error: ' + err);
})

We can make this even more beautiful with the arrow functions, which also came with ES6 a.k.a ES2015.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
validateToken(user, token) {
return openDatabase()
.then(db => getCollection(db, 'user_tokens'))
.then(collection => find(collection, { user: username }))
.then(result => result.token === token);
});
validateToken('marco', 'abc')
.then(valid => {
if (valid) {
console.log('awesome your token is valid!');
} else {
console.log('Nononoooo, that token is invalid!');
}
})
.catch(error => console.error('Sigh, something went wrong! Error: ' + err));

An arrow function returns by default, meaning you don’t have to specify the return keyword. You also don’t have to specify the curly braces if it is a one statement arrow function. This results in code with less syntax clutter. Way more readable code right? But as you can see the functions you are calling also have to return a Promise. So far we have seen that it can hugely improve our code, but we didn’t see yet how we can wrap an existing function with a callback API to return a promise. First have a look in next chapter on what a Promise actually is.

What is the promise

A Promise is used for asynchronous computations. It represents an operation that hasn’t completed yet, but is expected to complete in the future.

1
new Promise(/* executor */function (resolve, reject) { ... });

The executor is invoked immediately by the promise, causing you async logic to start. As soon the async logic finishes or errors the resolve or reject parameters will be used to resolve the Promise final value. The states of a Promise therefore can be one of the following three values: pending, fulfilled, rejected.
On the Promise prototype we have 2 methods available Promise.prototype.then and Promise.prototype.catch. Both methods will return a Promise making it possible to chain the method calls.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var asyncOperation = new Promise(function (resolve, reject) {
// Some async stuff...
if (successfull) {
resolve(result);
} else {
reject(err);
}
});
asyncOperation.then(doA)
.then(doB)
.catch(handleError)
.then(continueAfterError)
.then(doC)
.then(doD)
.catch(catchRemainingErrors);

Implement the promise

Lets take a look on how we can have the openDatabase method from earlier example return a Promise instead of using the callback approach.

1
2
3
4
5
6
7
8
9
10
11
function openDatabase() {
return new Promise(function (resolve, reject) {
mongoose.connect('mongodb://localhost/my-db', function (err, db) {
if (err) {
reject(err);
} else {
resolve(db);
}
})
});
}

Today more and more libraries support Promises by default, meaning you won’t have to apply above pattern in order to use Promises in your code. However there are still libraries out there which are still implementing the callback/errback approach. For those libraries you can easily wrap them in your own methods to return a Promise. Let me give you one more example using the fs module.

1
2
3
4
5
6
7
function readFile(filePath) {
return new Promise((resolve, reject) => fs.readFile(filePath, (err, data) => err ? reject(err) : resolve(data)));
}
readFile('/etc/hosts')
.then(data => console.log(data))
.catch(err => console.error(err));

For the lazy ones you can also have a look at Bluebird. Bluebird has some convenient utility methods which can change a methods prototype to return a Promise. See below for an example.

1
2
3
4
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs')); //Promisify all methods on the object
const redis = Promise.promisifyAll(require('redis')); //Promisify all methods on the object
const readFile = Promise.promisify(require("fs").readFile); //Promisify a single method

References:

So with all this in mind, there is nothing that keeps you from using Promises today. As well in the web browser as in Node.js. Even when you have to support an older web browser, you can still fall back on libraries such as Bluebird. Happy to get your feedback in the comments below.

Share