Understanding Promises in Node.js

A promise is a placeholder for a value that will be available in the future, so the result of an asynchronous task can be handled once it has finished. Promises make writing asynchronous code easier and are an improvement to the callback pattern (please google for callback hell). Since ES6 promises are a standard part of Javascript and with async/await (ES8) they are used in async functions .

What are promises?

To understand Promises it is important to understand the difference between synchronous and asynchronous code first.

Synchronous code executes in the sequence it is written, code statements wait until the ones before them have finished. Hence, synchronous code is considered blocking in Node.js. Blocking could be in some rare cases considered useful, like reading important configuration on start-up before anything else runs, but the application is unresponsive until this synchronous task is finished. Therefore, not applicable on long-running tasks, like making an HTTP call.

Asynchronous code works by starting a task, and letting it complete in the background while other code is still able to execute. When the async code has completed, the handler function (callback) is immediately executed with the result from the async code. Hence, asynchronous code is non-blocking , because it does not prevent the rest of your code from executing, while the asynchronous task is running in the background. With async code, we don't know when or if the task will complete successfully. The callback of the async code will be called as soon as the result is available, or when an error has occurred.

Once you an async process has started, like an HTTP request, filesystem access, or something similar, you are given something that will notify the caller when that process has completed. A Promise is that "something". A promise is a placeholder for a value that will be available in the future.

Why use Promises?

Promises allow to handle the results of asynchronous code, like callbacks. Unlike callbacks, the async code with promises is easier to read, maintain, and reason about. Consider these examples, five consecutive API calls with error handling.

Promises

fetch('url')
  .then(() => fetch('url'))
  .then(() => fetch('url'))
  .then(() => fetch('url'))
  .then(() => fetch('url'))
  .then(() => console.log('all done'))
  .catch(err => console.log(err));

Callbacks

fetchCallback('url', err => {
  if (err) return console.log(err);
  fetchCallback('url', err => {
    if (err) return console.log(err);
    fetchCallback('url', err => {
      if (err) return console.log(err);
      fetchCallback('url', err => {
        if (err) return console.log(err);
        console.log('all done');
      });
    });
  });
});

As you can see, the code is more legible with Promises.

Working with Promises

We can interact with the result of the Promise by chaining together handlers, that will either wait for the Promise to be fulfilled with a value, or rejected with the first error thrown.

fetch('url')
  .then(response => console.log(response.status))
  .catch(error => console.log(error));

In the above code example fetch returns a Promise, and the Promise API allows us to chain the then and catch handlers.

Your Promise chain should include a catch handler to deal with any Promises that are rejected in the chain. To handle errors with catch is best practice.

In future versions of Node.js, unhandled Promise rejections will crash your application with a fatal exception.

A Promise is in one of these three states:

  • pending : initial state, neither fulfilled nor rejected.
  • fulfilled : the operation was completed successfully.
  • rejected : the operation failed.

Creating a Promise

A new Promise can be created by initializing one with the Promise constructor:

const myPromise = new Promise((resolve, reject) => {
  // do something asynchronous
});

The Promise constructor takes two functions as arguments, resolve and reject. We can do the asynchronous task, and then call either resolve (with the result if successful) or reject(with the error). The constructor returns a Promise object, which can then can be chained with then and catch methods.

Let's have a look at some example:

const fs = require('fs');

const myPromise = new Promise((resolve, reject) => {
  fs.readFile('example.json', (err, data) => {
    if (err) {
      reject(err);
    } else {
      resolve(data);
    }
  });
});

myPromise
  .then(data => console.log(data))
  .catch(err => console.log(err));

In the code example above, we wrapped fs.readFile in a Promise. If reading the file encountered an error, we pass it to reject, otherwise we pass the data obtained from the file to resolve. Calling resolve passes the data to our .then handler, and reject passes the error to the .catch handler.

Chaining Promises

Combining multiple Promises is one of the big advantages of Promises over using callbacks. It is difficult to orchestrate multiple callbacks together, whereas with Promises it is much more readable, and error handling is standardized between the different Promises.

Let's have a look at an example for fetching the json placeholder API to get some todos.

fetch('https://jsonplaceholder.typicode.com/todos')
  .then(response => response.json())
  .then(json => console.log(json))
  .catch(err => console.log(err));

In the example above we fetch some JSON data via an HTTP request. The fetch function returns a promise, which will either resolve or reject. The attached then handles the response by fetch, when it resolves. The response body has a json method for parsing the response from JSON to an object. The json method returns a promise of its own, which handle by attaching another then handler, and in case of error we attach a catch handler and log the error.

TL;DR

  • Promises help deal with the execution flow of asynchronous code.
  • Promises are cleaner and more maintainable than using callbacks (in most cases).
  • A Promise can have one of three different states: pending, fulfilled, or rejected.
  • We can chain then and catch methods to a Promise in order to execute code when the state changes.
  • Promises can be used to execute synchronous operations without blocking the Node.js process.

Thanks for reading and if you have any questions , use the comment function or send me a message @mariokandut.

If you want to know more about Node, have a look at these Node Tutorials.

References (and Big thanks):

25