A quick guide to JavaScript Promises

October 03, 2016
Written by

OrXRdtlYz4AU01MZ3tm1n83BYw2ZHa6XpkhmH2mMrVPtBy-vDi4wFlSet-sG36ayfeM_Q33BYr1Q1_ZtWjynNfZNJqmhghCYZ_8gDdSP71mqiMdiktYsyLIncqG0y33X-tn_1ONA-4

icompile.eladkarako.com_callback_hell.gif

When you are writing JavaScript, callbacks are one of the most confusing concepts. Promises are the new approach to improve working with async code.

One of the biggest problems of callbacks is the chaining of different asynchronous activities. You end up calling anonymous function after function to pass around values. The result is an unmaintainable “callback hell”. Promises try to solve this problem but can be a bit confusing in the beginning.

Let’s define what Promises are, why they are incredibly useful and how to do things like executing calls in parallel or in series. For this we will look at different examples of doing HTTP requests using two different Node.js libraries.

Setup

Before we get started you need the following things to try our code examples:

  • Node.js version 6.0 or higher. You can check your version by running node -v in your command line. You can upgrade by downloading the latest binary from their website or by using a tool like nvm.

Once you have this, you need to create a new folder. I’ll create a promises folder in my home directory. Install the fetch and request libraries by running the following command in that folder:

npm install node-fetch request --save

Create a new file called promises.js and place the following lines in there to load the library:

'use strict';

const fetch = require('node-fetch');
const request = require('request');

We will be working out of the same promise.js file throughout the whole post.

Quick Promise Overview

To understand the benefits of Promises let’s first look at how to do an asynchronous call without promises. For this we will do an HTTP GET request using the request library.

Add the following lines to promises.js:

request.get('http://httpbin.org/get', (error, response, body) => {
  if (error) {
    console.error('Oh shoot. Something went wrong:');
    console.error(error.message);
    return;
  }

  console.log('Request done. Response status code: %d', response.statusCode);
});

Now run this code by executing the following command:

node promises.js

As you can see, we pass in the callback function as a second argument to request.get call. The library will automatically execute this function when the response for the HTTP request comes in. It will pass three arguments. The first argument is the potential error or null if it was successful. The second argument is the HTTP response and the third argument is the response body.

If we use fetch instead of the request.get we can leverage Promises as fetch will return a Promise instead of accepting a callback as a second argument. A Promise is an object that has two important methods: then() and catch(). then() can receive 1 or 2 arguments and catch() can be used to handle errors.

For then(), the first function argument is called if the result of the call was successful. The second function will be called if there was an error resolving the Promise. We’ll look into the difference between that error handler and catch() later.

Replace the previous code with the following to start using Promises:

fetch('http://httpbin.org/get')
.then(response => {
  console.log('Request using Promises done. Response status code: %d', response.status);
})
.catch(error => {
  console.error('Oh shoot. Something went wrong with the promise code:');
  console.error(error.message);
});

Re-run the code by executing again node promises.js.

So far there is no big difference from the callback code aside from it being a bit cleaner. The real magic comes when we want to do some data manipulation or make multiple calls. For this the general rule is that if the handler function that we pass to then or catch returns a value or another Promise, the Promise-chain will continue.

As an example add a function that extracts the status code and returns it:

function extractStatusCode(response) {
  return response.status;
}

fetch('http://httpbin.org/get')
.then(extractStatusCode)
.then(statusCode => {
  console.log('Request using Promises, part II. Response status code: %s', statusCode);
})
.catch(error => {
  console.error('Oh shoot. Something went wrong with the promise code:');
  console.error(error.message);
});

Run the code again. The output in the console should be the same but our code is more structured.

This code will first perform the HTTP request, then call the extractStatusCode function and once that function returned it will execute our anonymous function that will log the response status code.

Catching Errors

13ywPzPJdfhmBG.gif

Now that we are using Promises we might hit an issue. All of our code will fail silently if we don’t catch errors properly.

Imagine using Promises like wrapping your whole code into a try {} block. Your code will just silently fail unless you catch them explicitly. Catching errors is hugely important and not just ‘common courtesy’.

In order to properly catch errors we have two options. The first way is to pass a second function into our then() call.

Make the following changes to your code to test this:

function extractStatusCode(response) {
  return response.status;
}

fetch('invalid URL')
.then(extractStatusCode, errorInFetch => {
  console.error('An error occurred in the fetch call.');
  console.error(errorInFetch.message);
  // return null as response code since no request has been performed
  return null;
})
.then(statusCode => {
  console.log('Request using Promises. Response status code: %s', statusCode);
})
.catch(error => {
  console.error('This will never be executed');
});

When you run this code you’ll see that it will hit the error handler we added and print the respective messages to the screen:

Screen Shot 2016-09-29 at 3.13.27 PM.png

However it is not executing the catch handler because we are returning a value of null in the handler. From that point on the Promise chain is considered to be on the happy path again since the error has been handled.

We can make sure that it continues treating this as an error by throwing the error or returning by returning a new Promise using Promise.reject(error):

function extractStatusCode(response) {
  return response.status;
}

fetch('invalid URL')
.then(extractStatusCode, errorInFetch => {
  console.error('An error occurred in the fetch call.');
  console.error(errorInFetch.message);
  // forward the error
  return Promise.reject(errorInFetch);
})
.then(statusCode => {
  console.log('Request using Promises. Response status code: %s', statusCode);
})
.catch(error => {
  console.error('This will now be executed as another exception handler.');
});

Now that we know how to handle an error with then() what’s the difference between this and catch()?

To understand this let’s fix our fetch snippet again to use a valid url and instead break the extractStatusCode function by overriding response with undefined before accessing the status property:

function extractStatusCode(response) {
  response = undefined;
  return response.status;
}

fetch('http://httpbin.org/get')
.then(extractStatusCode, errorInFetch => {
  console.error('This will not be executed.');
  console.error(errorInFetch.message);
  // forward the error
  return Promise.reject(errorInFetch);
})
.then(statusCode => {
  console.log('Request using Promises. Response status code: %s', statusCode);
})
.catch(error => {
  console.error('There was an error somewhere in the chain.');
  console.error(error.message);
});

Screen Shot 2016-09-29 at 3.17.10 PM.png

The error handler in the then() part isn’t executed because this handler is only for the previous Promise and not the handler. However our catch() handler will be executed since it catches any errors that happen in the chain.

Executing in Parallel

obAMTQ7SeWqPK.gif

This is where the magic of Promises comes in. Consider the case in which we want to send multiple HTTP requests or do multiple asynchronous calls and want to know when they’re done.

The endpoints we want to request are held in an array. Using callbacks this can be quite a mess. To accomplish it we have to use counters in the callbacks to check if we are done and other similar hacks.

With Promises we can simply map over the array of messages, return the Promise in the map function and pass the resulting array into the built-in function Promise.all(). This will return a new Promise that resolves as soon as all calls succeed, or rejects once one of them fails.

const queryParameters = ['ahoy', 'hello', 'hallo'];

const fetchPromises = queryParameters.map(queryParam => {
  return fetch(`http://httpbin.org/get?${queryParam}`)
    .then(response => {
      // parse response body as JSON
      return response.json()
    })
    .then(response => {
      // extract the URL property from the response object
      let url = response.url;
      console.log('Response from: %s', url);
      return url;
    });
});

Promise.all(fetchPromises).then(allUrls => {
  console.log('The return values of all requests are passed as an array:');
  console.log(allUrls);
}).catch(error => {
  console.error('A call failed:');
  console.error(error.message);
});

If you run this code you should multiple requests being made. However there is no guarantee in which order the calls are run and finished as they are executed in parallel.

Executing in Series

26AHRsTEXSMgl3rXi.gif

While executing in parallel is cool and performant we sometimes have to make several calls in series due to restrictions or dependencies. We can also use Promises for this.

Chaining Promises when you know all necessary calls is super easy to do. However, it’s more complicated if we dynamically generate the asynchronous functions we need to execute.

There is a way we can get this done:

const queryParameters = ['ahoy', 'hello', 'hallo'];

let mostRecentPromise = Promise.resolve([]); // start with an immediately resolving promise and an empty list
queryParameters.forEach(queryParam => {
  // chain the promise to the previous one
  mostRecentPromise = mostRecentPromise.then(requestedUrlsSoFar => {
    return fetch(`http://httpbin.org/get?${queryParam}`)
      .then(response => {
        // parse response body as JSON
        return response.json()
      })
      .then(response => {
        // extract the URL property from the response object
        let url = response.url;
        console.log('Response from: %s', url);
        requestedUrlsSoFar.push(url);
        return requestedUrlsSoFar;
      });
  });
});

mostRecentPromise.then(allUrls => {
  console.log('The return values of all requests are passed as an array:');
  console.log(allUrls);
}).catch(error => {
  console.error('A call failed:');
  console.error(error.message);
});

The concept here is to chain the calls and execute the next one once the previous one resolves by wrapping it into a then() handler. This is the same approach we would do manually if we knew the amount of calls.

Right now we are using a forEach loop for this. This works but it isn’t really the most readable solution. To improve this we can use the reduce method of our array.

Modify the code accordingly:

const queryParameters = ['ahoy', 'hello', 'hallo'];

let mostRecentPromise = queryParameters.reduce((previousPromise, queryParam) => {
  return previousPromise.then(requestedUrlsSoFar => {
    return fetch(`http://httpbin.org/get?${queryParam}`)
      .then(response => {
        // parse response body as JSON
        return response.json()
      })
      .then(response => {
        // extract the URL property from the response object
        let url = response.url;
        console.log('Response from: %s', url);
        requestedUrlsSoFar.push(url);
        return requestedUrlsSoFar;
      });
  });
}, Promise.resolve([]));

mostRecentPromise.then(allUrls => {
  console.log('The return values of all requests are passed as an array:');
  console.log(allUrls);
}).catch(error => {
  console.error('A call failed:');
  console.error(error.message);
});

The overall approach here is the same as with the forEach loop. We specify a starting value of Promise.resolve([]) and call the reduce method on the messages array with a function that receives two arguments. One is the previous return value and the other is the current value of the array that we are accessing. This way we can reduce the array to a single value. In our case this will be the most recent Promise that we can then use to know when everything is done.

Turning Callback Code Into a Promise

Now that we know how to use Promises we have a problem to solve. What do we do with asynchronous code that doesn’t support Promises? For this we can wrap the function into a new function and use the new Promise() constructor. This constructor receives a function with two arguments:  resolve and reject. These arguments are functions we call when we want to resolve or reject a promise.

Here’s an example function that reads a file from disk and returns the content in a Promise:

const fs = require('fs');

function readFileWithPromise(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (err, content) => {
      if (err) {
        return reject(err);
      }
      return resolve(content);
    });
  });
}

readFileWithPromise('/etc/hosts').then(content => {
  console.log('File content:');
  console.log(content);
}).catch(err => {
  console.error('An error occurred reading this file.');
  console.error(err.message);
});

When we call new Promise() with a function as an argument, this function will immediately get executed asynchronously. We then execute fs.readFile with the necessary arguments. Once the callback of the readFile call is executed we check whether there is an error or not. If there is an error we will reject the Promise with the respective error. If there is no error we resolve the Promise.

Conclusion

Now you hopefully have a better idea of Promises and are ready to ditch the times of the old callback hell for some cleaner and more maintainable code. And if you are hooked you should check out what the future of JavaScript is going to bring with async/await to further improve asynchronous programming in JavaScript.

Also make sure to let me know what your experience with Promises is and why you love it (or hate it). Maybe you even have a crazy hack using Promises you want to show off? Just drop me a line: