What Are Promises Good For?

Posted on October 15, 2018

I’ve seen a lot of Javascript code that uses Promises. I’ve seen a lot of code that tries to shoe-horn a procedural style into Promises. And I’ve seen a lot of deeply nested Promise code. I’ve seen programmers sprinkle Promises on their code to, “make it async.” I’ve read about how async and await are supposed to solve the, “Promise problem.”

What is the Promise Problem?

The Problem with Promises

If language shapes the way you think and the kinds of ideas you can articulate then Javascript limits your ability to think about computations. There are few problems with Promises themselves but it is often in the way they are used that causes issues. It’s a misconception caused by Javascript’s lack of a sound, static type system that makes the problem difficult to articulate. Therefore you may see reference to Haskell in a few places to help clarify the ideas conveyed.

Do not be afraid, no knowledge of Haskell is required.

A Digression on Purity

Before I can explain what a computation is however we will have to understand purity. A pure function is a referentially transparent function. Another way to think about it is like a machine: a pure function takes some input and produces some output. For any given input it will produce, mechanically as a perfect machine would, a reliable output. Another way to think about it is that a function maps values from one set to another:

const sayDigit = num => {
    switch (num) {
        case 0:
            return "zero"
            break;
        case 1:
            return "one"
            break;
        case 2:
            return "two"
        // you get the idea
        default:
            return ""
            break;
}

Given then above definition we have a machine that takes in a number and gives us back a string. It happens that for numbers 0 through 9 it will give us back a string containing the english name for that digit and the empty string for any other number. It will do so reliably for any numeric input1. This is a pure function.

In contrast an impure function does not reliably return the exact same output for a given input. It is not a map between values of sets. When it is called the result may be different each time or it may cause some effect to occur such as writing to a file or sending a network request. Such functions are definitely useful otherwise computers wouldn’t be able to compute much and we wouldn’t be able to interact with them.

A Computation

Why does purity matter?

Because a computation is the result of causing an effect to happen. If I want to assign a value to a shared space in memory that’s a computation. If I want to reach out to my host browser and use its fetch API to ask for some data from a server: that’s a computation as well.

I receive a value by causing something to happen.

Back to Promises

With that digression out of the way we can understand what Promises are good for: computations!

However there’s a specific computation that Promises represent in Javascript that you may not be familiar with and it’s the source of the confusion that leads to messy, difficult-to-reason-about Promise-heavy code: IO.

IO stands for Input/Output. It encompasses all computations that interact with the world outside of your program: reading files from disk, fetching data from a web server, or painting things on a screen are all examples of IO computations. Some of them return data to us by the effect of calling them while others return nothing but cause something to happen outside of our program like painting pixels on a canvas.

Javascript doesn’t come with a static type checker or a sound type system which makes it difficult to see when our programs are performing IO computations. In fact, in Javascript, we’re used to interleaving such computations anywhere we want in our code. Contrast this with a language like Haskell where computations are enforced by the compiler!

-- An example of Haskell's type syntax
fetchUserFromGithub :: GithubID -> IO (Maybe GithubUser)
    ^                     ^        ^   ^
    |                     |        |   |
 our function name    a parameter  returning an IO computation
                                       |
                                       that may return a user

This funky thing is telling the Haskell compiler that our function is going to cause an effect to happen, by interacting with the world outside this program, in order to fetch a GithubUser. That’s all we need to know to understand how to use Promises in Javascript.

Promises are IO in Javascript

I can’t think of a good reason to use a Promise outside of performing some IO computations. If you need to read from the network, scan some files, write some output to a stream… you need to do this using IO computations. This is where you should be using Promises!

const fetchUserFromGithub = githubUserId =>
    fetch(`https://github.api/users/${githubUserId}`)
    .then(getUserFromResponse)
    .catch(handleFetchError)

The call to fetch returns a Promise. In fact this Promise returns a value to us by causing our computer to send some bytes to the world outside of our program by way of our browsers’ API which reaches into our host operating systems’ interfaces and so on. This function is not pure because whenever we call fetch it may return a different result. However we want to get the data back, in this case, by causing that effect to happen.

Combining Promises

Interleaving IO computations is limited with Promises. Our primary means of combination is the .then method. It takes a function that accepts to result of a computation and may return a value or another Promise. This is both useful and frustrating. We’ll see why in a moment but first another example function:

const storeGithubUser = githubUser =>
    fetch(myServiceUrl, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json; charset=utf8'
        },
        data: JSON.stringify(githubUser)
    })
    .then(returnResponseBody)
    .catch(handleFetchError)

This is another Promise-returning function. We can combine this one with our previous function using .then:

fetchUserFromGithub(GithubId(123))
    .then(storeGithubUser)
// => {response: {id: 393}}

When we want to sequence the effects one after the other this is how you do it. If you want to combine the results of computations from several sources as the input to another computation you’re in luck: you can use the .all method:

Promise.all([
    fetchUserFromGithub(GithubId(123)),
    fetchInstagramProfile(InstagramId(124))
])
.then(([githubUser, instagramProfile]) => {
    return storeUserWithInstagramProfile(githubUser, instagramProfile)
})

You get a couple more methods on the Promise object that you can read up on here.

But What About Purity?

Okay so you have some warm, fuzzy, pure functions. They do the business of transforming inputs into your desired outputs. They’re pure. They’re easy to test. They mean business. How do I use those with Promises?

This is where things get ugly in my opinion. You see due to some unfortunate decisions by the ECMAScript committee that introduced Promise to Javascript .then can take either a pure function or a Promise-returning impure one. This may seem convenient in small examples but I’ve worked with big, hairy Promise-wielding code bases and let me tell you: it isn’t pretty.

const userFullName = ({firstName = '', lastName = ''} = {}) =>
    `${firstName} ${lastName}`.trim()

fetchUserFromGithub(GithubId(123))
    .then(userFullName)
// => 'Bob Belcher'

Our function, userFullName is a pure function. Given any object that has the properties firstName and lastName it will return a string with those values interpolated2. The confusing thing is that we can add another .then onto then end of this and call a function that returns a computation… under the hood .then is doing a bit of introspection to see what we return and wraps a Promise around a value that isn’t itself a Promise.

How Do Promises Get Ugly?

The unfortunate decisions I mentioned earlier all combine to make it easy to write messy code with Promises that is hard to reason about. The sequential nature of Promise’s .then limits our combination of effects to happen sequentially. And because functions returning Promise are evaluated immediately there’s no way to compose Promises by normal means of function combination: all combination has to happen within the chain of .then methods.

Can we fix this?

Async and Await

You may have read about or heard people say that async and await fix Promises. What does that mean? Well for that let’s take another digression to Haskell:

storeUserData :: GithubID -> IO (Either StorageError UserID)
storeUserData githubId = do
    githubUser <- fetchGithubUser githubId
    linkedInProfile <- fetchLinkedInProfile $
        maybe "" githubUserEmail githubUser
    userId <- storeUserData githubUser linkedInProfile
    return userId

I don’t need to go into too much detail here and teach you Haskell in order to make this next point so don’t worry if you don’t understand everything that’s happening in this code sample. The important thing to note here is that do starts a block of code. The <- operator returns the result of the computation performed on the right to the name on the left. You can imagine, based on our previous examples, what the functions fetchGithubUser and fetchLinkedInProfile must look like. They all happen in IO!

And so async and await allow us to use this same idea in Javascript:

const storeUserData = async function(githubId) {
    const githubUser = await fetchGithubUser(githubId)
    const linkedInProfile = await fetchLinkedInProfile(
        githubUser.email || ''
    )
    const userId = await storeUserData(githubUser, linkedInProfile)
    return userId
}

Similar, right? The idea here is that your functions can return a single computation. We can assign the result of that effect by awaiting it in our special async function. The reason to use this notation is when our effects have dependencies or branches on their results:

const storeUserData = async function(githubId) {
    const githubUser = await fetchGithubUser(githubId)
    let linkedInProfile = null, result = EmptyResult(), userId = null
    for (let strategy in linkedInStrategies(githubUser)) {
        try {
            linkedInProfile = await maybeFetchLinkedInProfile(strategy)
        } catch (err) {
            console.log(`Error fetching LinkedIn profile: ${err}`)
            continue
        }
        if (linkedInProfile) {
            userId = await storeFullUserData(githubUser, linkedInProfible)
            return {result, userId, complete: true}
        }
    }
    userId = await storeIncompleteUserData(githubUser)
    return {result, userId, complete: false}
}

Do you see how the flow is different between the sequence of computations in a Promise and an async function?

In the chain of .then we can only pass the result of the prior effect through function parameters. This is what forces the sequential composition of computations: compute A then compute B from A then compute C from B. This last computation has no reference to A.

Whereas we can have many computations that may know about the result of A in an async function. The trade off is that we can’t combine async functions like we can with Promises.

What Promises Are Good For

They encapsulate IO computations. That’s what we should use them for. If we stick to this convention then whenever we see a Promise we know our program is performing computations: it’s computing a value from the result of making some effect happen in IO.

When we need to compose together these effects and each computation only depends on the result of the one prior you should use .then. You can kick off a computation that requires several independent results using .all. There are others but how to use them is an exercise left to you, dear reader.

And last if we have branches in our computations or many effects that rely on a prior computation then we should consider async functions. They’re similar to Haskell’s do syntax. However we can’t combine the results of computations from these functions in the same way.

How to Structure Programs with Promises

This is a long post. The general advice I have here is: use Promises for as little as you can get away with. Keep those IO computations at the edge of your program. It’s a pattern that has many names: functional core, imperative shell and the Haskell Three-Layer Cake. Build as much of your program logic using pure functions and read input from or output data to the world outside your program using Promises.

If you have computations that do not deal with IO avoid using Promise. Try using Either or Maybe or State and others. There are libraries that have stolen prodigiously from Haskell and other functional programming languages that implement these computations for you and they’re almost as difficult to grasp, but often not much more difficult, as Promises. Once you have explicit control over your computations it becomes easier to reason about and structure your programs.

Happy hacking!

  1. Although technically our input can be any type so we have to be careful ↩︎

  2. And converted to strings if you’re following the types ↩︎