The first article described the base information about promises.
Before we get to more complicated topics, I'd suggest thinking about the tools and utility functions we can create on top of promises. For this article let's think about getting the current promise status and building a promise queue.
TL&DR
Check promise status
const pending = {
state: 'pending',
};
function getPromiseState(promise) {
// We put `pending` promise after the promise to test,
// which forces .race to test `promise` first
return Promise.race([promise, pending]).then(
(value) => {
if (value === pending) {
return value;
}
return {
state: 'resolved',
value
};
},
(reason) => ({ state: 'rejected', reason })
);
}
Promise queue
class Queue {
_queue = Promise.resolve();
enqueue(fn) {
const result = this._queue.then(fn);
this._queue = result.then(() => {}, () => {});
// we changed return result to return result.then()
// to re-arm the promise
return result.then();
}
wait() {
return this._queue;
}
}
How to get current promise status
After you created a promise, you cannot get an info at runtime whether the promise is still pending
, fulfilled
or rejected
.
You can see the current promise status in debugger:
From the debugger we see that the Symbol [[PromiseState]]
is in charge of that. However, PromiseState
is not well-known symbol, so we can treat this state, as a private field which we can't get "right now".
However, we can use Promise.race
to test the current promise status. To do so, we may use the feature of Promise.race
:
📝 Promise.race checks promises in their order. For example:
const a = Promise.resolve(1);
const b = Promise.resolve(2);
Promise.race([a, b]).then(console.log); // 1
And at the time:
const a = Promise.resolve(1);
const b = Promise.resolve(2);
Promise.race([b, a]).then(console.log); // 2
📝 This code tests the promise status:
const pending = {
state: 'pending',
};
function getPromiseState(promise) {
// We put `pending` promise after the promise to test,
// which forces .race to test `promise` first
return Promise.race([promise, pending]).then(
(value) => {
if (value === pending) {
return value;
}
return {
state: 'resolved',
value
};
},
(reason) => ({ state: 'rejected', reason })
);
}
Usage https://codesandbox.io/s/restless-sun-njun1?file=/src/index.js
(async function () {
let result = await getPromiseState(Promise.resolve("resolved hello world"));
console.log(result);
result = await getPromiseState(Promise.reject("rejected hello world"));
console.log(result);
result = await getPromiseState(new Promise(() => {}));
console.log(result);
result = await getPromiseState("Hello world");
console.log(result);
})();
In addition to the check, this feature of Promise.race
can be helpful to run some code with timeouts. E.g:
const TIMEOUT = 5000;
const timeout = new Promise((_, reject) => setTimeout(() => reject('timeout'), TIMEOUT));
// We may want to test check if timeout is rejected
// before time consuming operations in the async code
// to cancel execution
async function someAsyncCode() {/*...*/}
const result = Promise.race([someAsyncCode(), timeout]);
How to build your own promise queue
Sometimes you have to execute different code blocks in some order one-after-another. It's useful, when you have many heavy async blocks, which you want execute sequentially. We should remember, that:
📝 JS is single-threaded and therefore, we can have concurrent execution, but not parallel.
To do so, we can implement promise queue. This queue would put every function after all previously added async functions.
Let's check this codeblock:
class Queue {
// By default the queue is empty
_queue = Promise.resolve();
enqueue(fn) {
// Plan new operation in the queue
const result = this._queue.then(fn);
// avoid side effects.
// We can also provide an error handler as an improvement
this._queue = result.then(() => {}, () => {});
// To preserve promise approach, let's return the `fn` result
return result;
}
// If we want just to understand when the queue is over
wait() {
return this._queue;
}
}
Let's test it:
// Work emulator
const emulateWork = (name, time) => () => {
console.log(`Start ${name}`);
return new Promise((resolve) => {setTimeout(() => console.log(`End ${name}`) || resolve(), time)})
}
// Let's check if the queue works correctly if the promise fails
const failTest = () => () => {
console.log(`Start fail`);
return Promise.reject();
}
const queue = new Queue();
queue.enqueue(emulateWork('A', 500));
queue.enqueue(emulateWork('B', 500));
queue.enqueue(emulateWork('C', 900));
queue.enqueue(emulateWork('D', 1200));
queue.enqueue(emulateWork('E', 200));
queue.enqueue(failTest());
queue.enqueue(emulateWork('F', 900));
However, when the promise gets rejected, we swallowed the error completely.
It means, if the real code fails, we may never know about it.
📝 If you work with promises, remember to use .catch
at the end of the promise chain, otherwise you may miss failures in your application!
To fix the situation we can either provide a callback for the constructor:
class Queue {
_queue = Promise.resolve();
// By default onError is empty
_onError = () => {};
constructor(onError) {
this._onError = onError;
}
enqueue(fn) {
const result = this._queue.then(fn);
this._queue = result.then(() => {}, this._onError);
return result;
}
wait() {
return this._queue;
}
}
Or we can re-arm the promise!
class Queue {
_queue = Promise.resolve();
enqueue(fn) {
const result = this._queue.then(fn);
this._queue = result.then(() => {}, () => {});
// we changed return result to return result.then()
// to re-arm the promise
return result.then();
}
wait() {
return this._queue;
}
}
The same test would report a error!
📝 Every promise chain may cause an unhandledRejection error.
If you want to experiment with the code we build: https://codesandbox.io/s/adoring-platform-cfygr5?file=/src/index.js
Wrapping up
Promises are extremely helpful when we need to resolve some complex async interactions.
In the next articles we will talk why JS promises are called sometimes as thenable
objects and continue our experiments.
Top comments (1)
Looks good but what is the practical application for this Promise queue. It would be hard to follow your article without proper context.