Back

Forever Functional: The hidden state of Promises

Forever Functional: The hidden state of Promises

When you work with promises, you know that they may be in different states, and sometimes you may need to learn the specific status of a given promise — but how to get it? JavaScript provides no ready, available solution for this problem!

So, in this article we’re going to figure out a way to determine the current status of a promise by using only available, standard properties and methods, so our code will be sure to work everywhere.

What are the possible statuses of a promise?

Promises can be in three states:

  • Pending, when they haven’t yet resolved to a value or rejected with an error
  • Fulfilled, when they have resolved to a value
  • Rejected, when they have rejected

Pending promises are said to be ”unsettled” while fulfilled and rejected promises are “settled`“.

We can readily see all three states with little coding:

const a = new Promise((resolve,reject) => { /* nothing */ });
// Promise {<pending>}
const b = Promise.resolve(22);
// Promise {<fulfilled>: 22}
const c = Promise.reject("bad");
// Promise {<rejected>: 'bad'}

It’s evident that promises internally keep track of their status in some way, but there are no visible, public properties we can access for it. If we try Object.keys(somePromise), we’ll get an empty array.

So, if we want to get the status of a promise, we need to find some way to do so by using the available functions; there’s no “backdoor” we can use for it. Fortunately, there’s a way to achieve this, as we’ll see next.

Implementing our method

Following the example of what we did in our previous Waiting For Some Promises? article, we’ll define an independent function and also modify the Promise.prototype to simplify getting the desired status.

Promise.prototype.status = function () {
  return promiseStatus(this);
};

The issue here is that we won’t be able to get the status synchronously; we’ll have to write an async function for that. How? We can use Promise.race() for that.

When you call Promise.race() with an array of promises, it settles as soon as any promise is settled. With that in mind, we can call it providing two promises: the one we care about and an already settled one. How would this work?

  • If Promise.race() resolves to the value returned by our already settled promise, the other is pending.
  • If Promise.race() resolves to anything else, it is either with a value (so, the promise is fulfilled) or to an error with some reason (the promise was rejected.)

So, we can write the following:

const promiseStatus = async (p) => {
  const SPECIAL = Symbol("status");

  return Promise.race([p, Promise.resolve(SPECIAL)]).then(
    (value) => (value === SPECIAL ? "pending" : "fulfilled"),
    (reason) => "rejected"
  );
};

(A tip for keen-eyed JavaScripters: we didn’t really need to specify async in the first line; a function that returns a promise is, by definition, already async.)

How does this work? We must ensure that our already settled promise resolves to a value no other code can resolve to. Using a symbol is the key; symbols are guaranteed to be unique.

So, if Promise.race() resolves to some value, if it was our symbol, it means that the promise we wanted to test is still pending. Otherwise, if the race resolves to any other value, the other promise is fulfilled. On the other hand, if the race ends with a rejection, we know for sure the tested promise is rejected. (You may want to review how .then() works.)

We did it! Our promiseStatus() method is an async one, but it is settled immediately. Let’s see how to test it!

Testing our method

First, we’ll need some dummy promises, and we can reuse the fake ones we worked with in the Waiting For Some Promises? article.

const success = (time, value) =>
  new Promise((resolve) => setTimeout(resolve, time, value));

const failure = (time, reason) =>
  new Promise((_, reject) => setTimeout(reject, time, reason));

The success() function returns a promise that will resolve to a value after some time, and the failure() function returns a promise that will reject with a reason after some time. Now we can write the following tests, which could be transformed into Jest unit tests if we so desire.

const ppp = success(200, 33);
const qqq = failure(500, "Why not?");

promiseStatus(ppp).then(console.log);   // pending
promiseStatus(qqq).then(console.log);   // pending

ppp.then(() => {  
  promiseStatus(ppp).then(console.log); // fulfilled
  promiseStatus(qqq).then(console.log); // pending
});

qqq.catch(() => {
  promiseStatus(ppp).then(console.log); // fulfilled
  promiseStatus(qqq).then(console.log); // rejected
});

Let’s analyze the results. The first two tests produce “pending” results because no promise has had time to settle yet. If we wait until ppp has resolved, its status becomes “fulfilled”, but qqq is still “pending”. If we then wait until qqq is settled, ppp is still “fulfilled” (no change there) but now qqq is “rejected” — all correct!

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

Typing our method

We must start with our promiseStatus() function. We can define an auxiliary PromiseStatus type with the three possible results. Our promiseStatus() function will take a Promise<any> as a parameter and return a Promise<PromiseStatus> result.

type PromiseStatus = "pending" | "fulfilled" | "rejected";

const promiseStatus = (p: Promise<any>): Promise<PromiseStatus> => {
  const SPECIAL = Symbol("status");

  return Promise.race([p, Promise.resolve(SPECIAL)]).then(
    (value) => (value === SPECIAL ? "pending" : "fulfilled"),
    (reason) => "rejected"
  );
};

To modify the Promise.prototype, we must add a global definition as follows.

declare global {
  interface Promise<T> {
    status(): Promise<PromiseStatus>;
  }
}

With this definition, we can now add our function to the Promise.prototype.

Promise.prototype.status = function () {
  return promiseStatus(this);
};

With this, we would be able to work directly with the status method, as in:

ppp.status().then(console.log); 

or

const rrr = await qqq.status();
if (rrr === ...) { ... }

We’re done!

Conclusion

In this article, we’ve tackled the problem of learning a promise’s status and found a roundabout way to get at it because of limitations that didn’t allow us to directly access it. We’ve also produced a TypeScript fully typed version to make the code more useful. We’ll be returning to promises in future articles; there’s more to be said!

A TIP FROM THE EDITOR: We’ve covered promises in more articles, such as Waiting With Promises and Waiting For Some Promises?; don’t miss them!

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs and track user frustrations. Get complete visibility into your frontend with OpenReplay, the most advanced open-source session replay tool for developers.

OpenReplay