DEV Community

Cover image for A quick guide to JavaScript Promises
Dominik Kundel
Dominik Kundel

Posted on • Originally published at twilio.com

A quick guide to JavaScript Promises

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

Decorative GIF

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
Enter fullscreen mode Exit fullscreen mode

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

const fetch = require('node-fetch');
const request = require('request');
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

Now run this code by executing the following command:

node promises.js
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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

Decorative: GIF of little girl failing to catch a ball

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');
});
Enter fullscreen mode Exit fullscreen mode

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:

Screenshot of Terminal with error message

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.');
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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

Decorative GIF: hacking scene with two people coding on one keyboard

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);
});
Enter fullscreen mode Exit fullscreen mode

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

Decorative GIF: People standing in a queue bumping into each other

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

Recently Node.js also introduced a new function called promisify in the built-in util module. This function allows you to easily turn built-in callback-based functions into promise-based functions. Check out the documentation for more information.

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:


A quick guide to JavaScript Promises was originally published on the Twilio Blog on October 3, 2016.

Top comments (10)

Collapse
 
justsml profile image
Daniel Levy

Excellent article, I only have one suggestion.
Avoid using either .forEach or .reduce for sequential promise execution - at least when overwriting references to the promises.

Either use plain function recursion, or use a lib like bluebird.

Bluebird has features builtin for exactly this situation, including .mapSeries and concurrent execution limits, like .map(fn, {concurrency: CPU_COUNT})

Example with Bluebird:


const Promise = require('bluebird')

const searchTerms = (terms) => Promise
  .resolve(terms)
  .map(q => `http://httpbin.org/get?${q}`) // transform to needed func params for next step(s)
  .tap(url => console.time('Load %s', url)) // Easy to add/remove metrics
  .mapSeries(url => fetch(url) // -> Nested dependent logic:
    .then(resp => resp.json()) // unpack JSON
    .then(resp => { // Final step
      let url = resp.url;
      console.timeEnd('Load %s', url);
      return resp.url; // {url: resp.url, body: resp.body};
    })
  )

searchTerms(['ahoy', 'hello', 'hallo'])
  .then(allUrls => {console.log('Successfully loaded URL(s):', allUrls))
  .catch(error => console.error('A call failed:', error.message))

Collapse
 
justsml profile image
Daniel Levy

Also, another example of bluebird features, promisify existing callback-based libs using a common method.

So to read a file w/ Promises, use:

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

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

Collapse
 
dkundel profile image
Dominik Kundel

Good point! I mainly wanted to explain how to handle it without the help of an additional tool in a still elegant solution. I think a recursive function wouldn't make it too readable. But yeah I agree that you should ideally use something that is a bit more sophisticated. Bluebird is certainly a good solution for this.

Collapse
 
carlflor profile image
Carl Flor

Thanks for this! Great article! :)

Collapse
 
dkundel profile image
Dominik Kundel

Thank you! :)

Collapse
 
oneearedmusic profile image
Erika Wiedemann

Fantastic article, thanks! Shared around the office

Collapse
 
dkundel profile image
Dominik Kundel

Thanks a lot! :)

Collapse
 
jess profile image
Jess Lee

THANK YOU!

Collapse
 
dkundel profile image
Dominik Kundel

Glad you enjoyed it :)

Collapse
 
rpalo profile image
Ryan Palo

This is amazing! This is exactly what I needed, thank you. 😬