Blog.

From errbacks to Promises in Node

Marco Franssen

Marco Franssen /

6 min read1097 words

Cover Image for 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.

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?

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.

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.

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.

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.

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.

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.

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.

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.

You have disabled cookies. To leave me a comment please allow cookies at functionality level.

More Stories

Cover Image for Run your Angular app in a Nginx Docker container

Run your Angular app in a Nginx Docker container

Marco Franssen

Marco Franssen /

Today you will learn how we can package our static html Angular app in a Docker container running Nginx. By packaging our app in a Docker container we will benefit from the fact that we will have some immutable infrastructure for our app. Immutability will give you many benefits when it boils down to maintaining a platform. Things that can not change state also can't lead to surprises in a later stage. Immutability is also well known in functional programming languages. I won't list all the adva…

Cover Image for Setting up Docker development environment on Windows/Mac

Setting up Docker development environment on Windows/Mac

Marco Franssen

Marco Franssen /

In this post I want to cover how you can setup a Docker development environment on Windows/Mac. As you might already know Docker requires a Linux kernel to run. Therefore we will always need a VM to run the actual Docker environment when you are on Windows or Mac OS. Hereby a few quotes from the Docker webpage to remember you what Docker is all about, or to give you a quick idea. Docker provides a common framework for developer and IT teams to collaborate on applications. With a clear separatio…

Cover Image for Ssl certificate for your Azure website using Letsencrypt

Ssl certificate for your Azure website using Letsencrypt

Marco Franssen

Marco Franssen /

Letsencrypt is a free automated service which provides you SSL certificates for free. Although the certificates are only valid for 3 months, this shouldn't be a bottleneck as you can fully automate the certificate request and renewal. Letsencrypt comes with a python client which you can use to make a certificate request. Letsencrypt can be ran from a Linux OS. In this blogpost I will show you how to use the Letsencrypt Vagrant box (Ubuntu vm) to authorize the certification request for your Azur…

Cover Image for Switch between Hyper-V and Virtualbox on Windows

Switch between Hyper-V and Virtualbox on Windows

Marco Franssen

Marco Franssen /

Since a while I have been using Virtualbox + vagrant to do web development based on Linux, Nginx and NodeJS. However I also still do Windows development occasionally. For that reason I needed a way to easily switch from Virtualbox to Hyper-V or the other way around, as Hyper-V is required for example for the Windows Phone emulator. Hyper-V can not run together with Virtualbox as they both need an Hypervisor. Unfortunately you can't have 2 Hypervisors running. Therefore we need to disable Hyper-…