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 errorFulfilled
, when they have resolved to a valueRejected
, 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!