Promises in JavaScript: A Gentle Introduction
tl;dr
Personally, I think you should know what Promises are, but you don't really need to know the
internals to be a fairly productive developer. You can just use axios
or
fetch
and call then
on the results a couple times and scrape by. OR
you could read the rest of this post and get friendly with Promises if you
want to be awesome.
Asynchronous Behavior, Callbacks, and Why You Should Get Better Acquainted with Promises
Lately, I've been reading about Promises in JavaScript (major shoutout to Kyle Simpson and his You Don't Know JS series), so I figured it was time to write a blog post about what I've learned, especially since Promises are now native in ES6.
That's great, you're probably thinking, but why do I need to know this? The answer is that Promises simplify the handling of asynchronous behavior in JavaScript and have the potential to eradicate callback hell as we know it. Never encountered callback hell? You probably have, but just didn't realize it had the potential to be as hellish as it is. Any time you have a series of asynchronous actions and need deterministic values to be returned, you're probably doing some crazy gating or latching technique in order to make sure that the data you want is present.
For instance, let's say you want to do something like this:
// code that won't actually work
let a = ajax('http://some.url')
let b = ajax('http://some.other.url')
function addResponse() {
return a + b
}
addResponse()
You make two ajax calls and want to return the value of both the calls. The
problem is that you don't know exactly when both a
and b
will be
available. In order to work around this problem, you could write something
like this using callbacks (and a gate):
let a = ajax('http://some.url', sum)
let b = ajax('http://some.other.url', sum)
function sum() {
if (a && b) { //cool gate #amirite
return a + b
}
}
This way, we wait until both values resolve before summing them. This approach, though, could be improved and clarified with Promises. As Kyle Simpson points out in chapter 2 of his book, Async and Performance, "It turns out that how we express asynchrony (with callbacks) in our code doesn't map very well at all to that synchronous brain planning behavior." In other words, using callbacks isn't intuitive to our sequential, planning brains. Thus, we turn to Promises.
What Are Promises?
At their core, Promises are just a way of expressing a future value. Simpson likens a Promise to a receipt or IOU at a fast food restaurant. You request something and you get a promise that you'll eventually receive something back. (Going forward, I'm going to stop capitalizing the word "promises." I think you get it.)
Promise Basics
According to the MDN Docs, the syntax for a promise is as follows:
new Promise ( /* executor */ function(resolve, reject) { ... })
The executor is just a function that is passed the arguments resolve
and
reject
. Usually the executor does some asynchronous stuff. When it's done,
it'll call the resolve
function with a final value. If an error gets
thrown, reject
will be called instead. Either way, with Promises, you're
guaranteed to get some sort of value, which eliminates the "inversion of
control" problem of callbacks. (When you use callbacks, you're effectively
passing execution of your program, so functions could get called once as
intended, too many times, or never at all.)
Promises have three states: pending, fulfilled, and rejected. Pending means that it's neither fulfilled or rejected, fulfilled means that the operation was successful, and rejected means that the operation failed.
How to Use Promises
If you were writing your own async operation, you'd probably write something like this:
function fetchSomeData() {
return new Promise(function(resolve, reject) {
// an operation to async
let request = new XMLHttpRequest()
request.open('GET', '/someUrl')
request.onload = function () {
resolve(this.responseText)
// when the loading is done, it'll resolve
// with the value
}
request.send()
})
}
Now, you can call
fetchSomeData()
.then((data) => {
console.log(data)
})
and the data should print out to your console (when it's good and ready).
You can imagine that this is how a lot of libraries such as fetch
or
axios
likely end up implementing asynchronous calls with promises.
Then: Another Cool Thing
Another fun thing about then
in the promise(d) land is that whenever you
call then
on a promise, it will return another promise that resolves with
the return value of the callback.
For example:
var p = function sayHello() { // just to illustrate that `p` references the `sayHello` function
return new Promise(function(resolve, reject) {
var hello = "hello"
resolve(hello) // here I'm resolving the promise with the string "hello"
})
}
p().then((data) => console.log(data))
// #=> "hello"
// the `data` argument is the value that the promise resolved with
p()
.then((data) => data + " there") // this implicit return in ES6 returns "hello there"
.then((moreData) => console.log(moreData)
// #=> "hello there"
How You'll Use Promises Most of the Time
So as you can see, promises are pretty awesome and they really want to be your friend. But to get started using promises, you probably don't need to know all of this cool stuff.
Most of the time, you'll probably be using libraries such as fetch
or
axios
, which make use of the promise API and provide some nice wrappers
for handling asynchronous requests.
Personally, I prefer using axios
for my projects because you don't have to
resolve two promises, one of which is streaming. (Full disclosure: I have no idea what that
is.)
Anyway, to use axios
in your project, you'd need to require the node module
into your project via yarn
or npm
. Then, assuming you're using some kind
of CommonJS-like bundler, import the package with
webpack, gulp, or browserify.
Wherever you need to make a request in your project, you could write something as simple as this to get some data and use it:
axios.get('api/v1/ice-creams')
.then((response) => console.log(response))
And boom! No need to worry about resolving more than one promise. Axios handily wraps up all that code for you and provides a super simple API.
Cool Promise Tricks
One of the coolest promise tricks I've come across, though, is
Promise.all
, which allows you to dispatch multiple asynchronous actions
and wait for all of them to resolve before proceeding to the next step.
According to the Promise.all documentation, the method takes in an array (or iterable) of promises, and returns a single promise that resolves when all of the promises in the iterable (array) have resolved. Or it rejects the first promise that rejects.
So, one good use case for Promise.all
, which I coincidentally encountered
in the codebase I'm currently working on, is fetching data from two
endpoints and merging them client-side.
The code looks something along the lines of this:
let promiseA = fetch('/url?includes=someOfTheThings')
let promiseB = fetch('/url?includes=theRestOfTheThings')
let promiseC = fetch('/url?includes=stillMoreThings')
Promise.all([promiseA, promiseB, promiseC]).then((values) => {
// values is an array of the results of the resolution of the each promise
console.log(value[0] + value[1] + value[2])
})
In the real code, we do some complex merging with the help of the lodash
library.
Anyway, that's promises in a nutshell. For a more in-depth discussion of promises, I highly recommend reading chapter 3 of Kyle Simpson's You Don't Know JS: Async and Performance. He breaks all of this down in further detail, provides a lot of great examples, and also covers error handling, which I completely glossed over, much like any shipping-focused developer would.