Promises are a built-in concurrency primitive that's been part of JavaScript since ES6 in 2015. The Promise class has been included in Node.js since v4.0.0. That means, unless you're on an unmaintained version of Node.js, you can use promises without any outside libraries.

Getting Started with Promises in Node.js

A promise is an object representation of a value that is being computed asynchronously. The easiest way to create a promise is using the Promise.resolve() function.

// Create a promise with a pre-computed value 'Hello, World'
const promise = Promise.resolve('Hello, World!');

promise instanceof Promise; // true

The most important function for interacting with promises is the Promise#then() function. To get the promise's associated value once it is done being computed, you should call .then():

// Prints "Hello, World"
promise.then(res => console.log(res));

There is no way to access a promise's value directly. In other words, the only way to "unbox" promise into the value 'Hello, World' is by calling .then().

The Technical Details of Promises

You can think of a promise as an object representation of an asynchronous operation. Technically, a promise is a state machine representing the status of an asynchronous operation. A promise can be in one of 3 states:

1) Pending. The operation is in progress.

2) Fulfilled. The operation completed successfully.

3) Rejected. The operation experienced an error.

Here's a state diagram from Mastering Async/Await that demonstrates the possible states:

Once a promise is either fulfilled or rejected, it stays fulfilled or rejected. That is why a promise that is no longer pending is called settled.

Another important detail is that, as the client of a promise-based API, you can't fulfill or reject a promise. Given a promise object, you cannot make the promise change state, even if it is pending. You can only react to what the promise does using functions like then().

Speaking of then(), let's take another look at then() given the perspective of a promise as a state machine. The first parameter to then() is a function called onFulfilled.

promise.then(function onFulfilled(res) {
  console.log(res); // 'Hello, World'
});

As the name onFulfilled implies, Node.js will call your onFulfilled function if your promise changes state from pending to fulfilled. If you call then() on a function that is already fulfilled, Node.js will call onFulfilled immediately.

Errors and catch()

If promise changes state from pending to fulfilled, Node.js will call your onFulfilled function. What happens if promise changes state from pending to rejected?

Turns out the .then() function takes two function parameters, onFulfilled and onRejected. Node.js calls the onRejected function if your promise changes state from pending to rejected, or if the promise is already rejected.

promise.then(
  function onFulfilled(res) { console.log(res); },
  function onRejected(err) { console.log(err); }
);

If onRejected is null or undefined, Node.js will not treat that as an error. If a promise is rejected and it doesn't have an onRejected handler, that will become an unhandled promise rejection.

Promises also have a Promise.reject() function analagous to Promise.resolve(). Here's an example of using Promise.reject() with an onRejected handler.

Promise.reject(new Error('Oops!')).then(null, function onRejected(err) {
  console.log(err.message); // Oops!
});

The .then(null, function onRejected() {}) pattern is so common in promises that there's a helper function for it: Promise#catch(). The catch() function lets you add an onRejected handler without passing an onFulfilled() function.

Promise.reject(new Error('Oops!')).catch(function onRejected(err) {
  console.log(err.message); // Oops!
});

Chaining Promises

Promise chaining is what lets promises avoid deeply nested callbacks, also known as "callback hell", "pyramid of doom", and "banana code".

The idea of promise chaining is that if you call then() with an onFulfilled() that function returns a new promise p, then() should return a promise that is "locked in" to match the state of p.

For example:

const p1 = Promise.resolve('p1');
// The `then()` function returns a promise!
const p2 = p1.then(() => Promise.resolve('p2'));

// Prints "p2"
p2.then(res => console.log(res));

In practice, promise chaining generally looks like a list of chained .then() calls. For example:

return asyncOperation1().
  then(res => asyncOperation2(res)).
  then(res => asyncOperation3(res)).
  // If any of `asyncOperation1()` - `asyncOperation3()` rejects, Node will
  // call this `onRejected()` handler.
  catch(function onRejected(err) { console.log(err); });

Now that you've seen the basics of promise chaining, lets see how you would use promise chaining with core Node.js APIs.

Using Promises with Core Node.js APIs

Node.js 10 introduced a promise-based API for the built-in fs module. That means you can read from and write to the file system using promises in Node.js.

To use the promise-based fs API, use the following syntax:

const fs = require('fs').promises;

Now, suppose you want to read a file, replace all instances of 'foo' with 'bar', and write the file back to disk. Here's how that might look with promise chaining.

const fs = require('fs').promises;

function replace(filename) {
  let handle;
  let contents;
  // Open a file handle
  return fs.open(filename, 'r+').
    then(_handle => {
      handle = _handle;
      // Read the contents of the file
      return handle.readFile();
    }).
    // Replace instances of 'foo' with 'bar' in the contents
    then(_contents => {
      contents = _contents.toString().replace(/foo/g, 'bar');
    }).
    // Empty out the file
    then(() => handle.truncate()).
    // Write the replaced contents to the file
    then(() => handle.writeFile(contents)).
    then(() => console.log('done')).
    // If any step fails, this `onRejected()` handler will get called.
    catch(err => console.log(err));
}

Converting Callbacks to Promises

Many older Node.js APIs are still written using callbacks. Node.js has a promisify() function that can convert a function that uses Node.js' callback syntax to a function that returns a promise.

const util = require('util');

function usingCallback(callback) {
  // Call `callback()` after 50 ms
  setTimeout(() => callback(null, 'Hello, World'), 50);
}

const usingPromise = util.promisify(usingCallback);

// Prints "Hello, World" after 50ms.
usingPromise().then(res => console.log(res));

Moving On

Promises have replaced callbacks as the fundamental concurrency primitive for Node.js. Promises are the basis for async/await in Node.js, so your options are limited to promises, callbacks, or non-standard userland libraries. Now that the Node.js core APIs are starting to adopt promises, Node.js-style callbacks are fast becoming obsolete.

Confused by promise chains? Async/await is the best way to compose promises in Node.js. Await handles promise rejections for you, so unhandled promise rejections go away. My new ebook, Mastering Async/Await, is designed to give you an integrated understanding of async/await fundamentals and how async/await fits in the JavaScript ecosystem in a few hours. Get your copy!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus