A quick guide to JavaScript Promises
Time to read: 6 minutes
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:
Create a new file called promises.js
and place the following lines in there to load the library:
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
:
Now run this code by executing the following command:
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:
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:
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
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:
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:
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 throw
ing the error or returning by returning a new Promise using Promise.reject(error)
:
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:
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
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.
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
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:
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:
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:
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:
- Email: dkundel@twilio.com
- Twitter: @dkundel
- GitHub: dkundel
- Twitch (streaming live code): twilio
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.