Let's talk about how to talk about promises

Did you know...

  • ...that "fulfilling" a promise and "resolving" a promise aren't the same thing?
  • ...that a promise can be both "pending" and "resolved" at the same time?
  • ...that lots of your code is creating these pending resolved promises?
  • ...that when you resolve a promise you might be rejecting it rather than fulfilling it (or neither)?

If you answered "yes" to all of the questions above, there's probably no new information for you in this post. But if you answered "no" or "I don't know" to any of those, read on!

First off: You are not alone. "Resolve" is probably the most misunderstood word in promise-land. I completely misunderstood it when I first ran into promises some years back. People think "resolve" and "fulfill" are synonyms, but they aren't, and since the words "resolve" and "fulfill" are part of the JavaScript promise API¹ and not just some arbitrary terms I've picked, it's important to understand the difference.

A promises's primary state is one of three mutually-exclusive values:

  • pending - the initial state of most promises, it hasn't been fulfilled or rejected
  • fulfilled - the promise has been fulfilled with a fulfillment value
  • rejected - the promise has been rejected with a rejection reason (saying why the promise can't be fulfilled)

For convenience, we also use the collective term "settled" to mean "fulfilled or rejected."

Following on from that, we can say that:

  • You fulfill a promise (with a fulfillment value)
    or
  • You reject a promise (with a rejection reason explaining why it can't be fulfilled)

Notice that the word resolve isn't anywhere in the above. Contrary to popular belief, resolving a promise doesn't necessarily change its primary state. In fact, it often doesn't. Promise resolution is a separate concept from promise fulfillment.

So what's resolve then? When you resolve a promise, you determine what will happen to that promise from then on. When you resolve a promise with something like 42 or "answer" or {"example": "result"}, yes, you do fulfill the promise with that value. But if you resolve your promise to another promise (or more generally a thenable), you're telling your promise to follow that other promise and do what it does:

  • If the other promise is fulfilled, your original promise will fulfill itself with the other promise's fulfillment value
  • If the other promise is rejected, your original promise will reject itself with the other promise's rejection reason
  • If the other promise never settles, your original promise won't either

Regardless of what happens, though, there's nothing further you can do to the promise to affect the outcome. The promise is resolved to the other promise, irrevocably. Any attempt to resolve it again, or to reject it, will have no effect.

Now, you might be thinking of the resolve function the new Promise callback receives as an argument and saying "Well, okay, but I don't think I've ever passed a promise to resolve. That's just a niche use case." Fair enough! But that's just one way you resolve a promise, and probably not the main one. Probably the main one is returning something from a promise handler function, and I bet you've returned promises from handler functions a lot. Consider this, assuming first and second return promises:

function doStuff() {
    return first()
    .then(firstResult => {
        return second(firstResult);
    });
}

// ...

doStuff()
.then(result => {
    // ...use `result`...
});
.catch(error => {
    // ...handle/report error...
});

(Yes, that first then call could be just .then(second). The goal here is clarity.)

Before async functions came around, you probably wrote code like that all the time. Guess what? It creates a pending resolved promise. Here's how:

  1. When you call doStuff, it calls first which creates and returns a promise (Promise A).
  2. When you call then on that promise, then creates and returns another promise, the one that doStuff returns (Promise B).
  3. Let's assume that at some point, the promise from first is fulfilled. The fulfillment handler in doStuff calls second with that fulfillment value and returns the promise second gives it (Promise C). That resolves Promise B to Promise C. From that point forward, Promise B follows Promise C and does what it does.

In the normal case, Promise C won't be settled yet when that code resolves Promise B to it, so Promise B remains pending (neither fulfilled nor rejected), but it's also resolved (nothing can change what's going to happen, it's going to follow Promise C no matter what). If/when Promise C is settled, Promise B will settle itself the same way.

The same thing happens in an async function, since async functions are syntax for creating and consuming promises. Let's look at how we might write doStuff using async/await:

async function doStuff() {
    const firstResult = await first();
    return second(firstResult);
}

If the promise from first is fulfilled, doStuff calls second and then resolves its promise to the promise second returns. At that point, until/unless second's promise is settled, the promise from doStuff is both pending and resolved. It will fulfill itself, or reject itself, when/if second's promise settles.

That's the difference between resolving and fulfilling a promise.

You might be wondering, "Why use the word 'resolved' when things are still up in the air?" It's because of the irrevocability I mentioned earlier: once a promise is resolved, nothing can change what's going to happen to it. If it's resolved with a non-promise value, it's fulfilled with that value and that's that. If it's resolved to a promise, it's going to follow that other promise and that's that. You can't change its resolution, or reject it directly. Often, you don't have any way to even try to do that, but even if you do have the promise's resolve and reject functions from the new Promise callback, neither of them does anything once the promise is resolved. It's on a particular course, and while the outcome of the course it's on may not be clear yet, you can't change the course it's on. (You'll sometimes hear resolved/unresolved called "fates" to differentiate them from the states "pending," "fulfilled," and "rejected." For me that's a bit contrived but it may still be useful.)

So to round up, some verbs:

  • fulfill - to settle with a fulfillment value
  • reject - to settle with a rejection reason saying why the promise can't be fulfilled
  • settle - to fulfill or reject (I'm aware this is circular and I'm fine with that 😉)
  • resolve - to either

    • make a promise follow another promise (typically resolve to, which is Promises/A+ spec terminology)
      or
    • fulfill it with a value (I typically use resolve with in this case, if I know I'm working with a non-thenable, though that may be just my personal convention)

Some adjectives for a promise's primary state:

  • pending - neither fulfilled nor rejected
  • fulfilled - fulfilled with a fulfillment value
  • rejected - rejected with a rejection reason saying why the promise can't be fulfilled

And some further adjectives:

  • settled - convenience term for "fulfilled or rejected"
  • resolved - either settled or following another promise that will determine what happens to this promise
  • unresolved - not resolved (and thus can be resolved)

Just a quick coda before we go:

I've mentioned two of the ways you resolve promises, 1) calling the resolve function you get from new Promise and 2) returning a value from a promise handler callback. A third way you resolve a promise is by using Promise.resolve. Promise.resolve creates a promise that's resolved to what you pass into it. One of its primary use cases is where you don't know what you're going to receive — a native promise, a non-native promise from a library like Q or jQuery, a thenable, or a non-thenable value. By passing any those through Promise.resolve and consuming the resulting promise, you can treat them all the same way:

Promise.resolve(input)
.then(x => {
    // ...
})
// ...

In that example, input can be just about anything, and when you get x in the fulfillment handler, you know that A) if input was a promise of some kind, it was fulfilled; and B) x is not a promise or thenable, so you can work with it as a value.

Happy coding!


¹ resolve is the canonical name for the first function passed to the the new Promise callback, and Promise.resolve is a standard library function. The standard library function Promise.allSettled uses "fulfilled" to identify fulfilled promises.

Have a question or comment about this post? Ping me on Mastodon at @tjcrowdertech@hachyderm.io!