Promise is a then-able object. JS has native Promise impl, which is aligned with Promises/A+ standard https://promisesaplus.com/
new Promise((resolve, reject) => {...})
The constructor receives 2 arguments: resolve to fulfill the promise and reject to reject it.
๐ Promise can be in 1 of 3 different states: pending, fulfilled, rejected.
๐ Once a promise changes its state from pending to either fulfilled or rejected, it cannot be changed again
const result = new Promise((resolve, reject) => {
resolve(1); // pending => fulfilled with value === 1
resolve(2); // promise is already fulfilled. No effect
resolve(3); // promise is already fulfilled. No effect
});
result.then((value) => console.log(value)); // 1
Here we use .then
, one of the most important elements in promises:
function onResolve(value) {}
function onReject(reason) {}
new Promise((resolve, reject) => {...}).then(onResolve, onReject)
then
callback allows you to execute code when the previous promise is fulfilled or rejected.
๐ Every .then
callback can be called only once. There are 2 possible scenarios
- Promise gets fulfilled
- Promise was fulfilled before we call
.then
, so the callback will be executed in microtask after we reach the end of the current task and execute previously planned microtasks
// we enter to the main task
let resolve;
const result = new Promise((_resolve) => {
resolve = _resolve;
});
setTimeout(() => {console.log(3)}, 0); // Plan a timeout,
// which will be executed in a separate macro task
result.then(value => console.log(1)); // Plan an onResolve callback
// to the promise, which is in pending state
resolve('Hey'); // resolve the promise with the value == 'Hey'
// At this point, all previously planned `.then` callbacks
// will be placed in microtask queue.
// We will have microtask queue:
// value => console.log(1);
result.then(value => console.log(2)); // Put a new callback
// Microtask queue:
// value => console.log(1);
// value => console.log(2)
// End of macrotask
// Execute 2 microtasks one after another and print 1, 2
// End of all planned microtasks
// Execute a new macrotask from timeout
// print 3
๐ Every single then
call returns a new promise instance. It allows us to "chain" promises:
๐ .then
callbacks are executed in microtask queue:
If we have 3 macrotasks in the queue they will be executed one after another:
However if we plan a microtask during the execution one of the tasks this microtask will be executed right after current macrotask:
If code plans a microtask during the second task execution, this microtask will be executed right after the second task end, but before the third task, despite the third task might be placed in a queue earlier.
๐ Microtasks postpones macrotask execution
An infinite promise chain may "freeze" the tab:
function freeze(value) {
console.log(value)
return Promise.resolve(value + 1)
.then(freeze); // We have async operation here
// but it plans a new microtask,
// which prevents any other macrotask from the execution,
// including UI updates
}
freeze(1);
As promises are executed in microtask queue, they have slightly different error handling.
Let's take a closer look at it.
To handle an error we can either provide the second callback to .then
method or use .catch
.
Generally speaking .catch
is an alias for .then without the first argument:
.catch(onReject)
// is equal to:
.then(value => value, onReject)
๐ When promise gets rejected, it ignores all onResolve
callbacks up to the first onReject
handler
Promise.reject('fail')
.then(value => console.log(1)) // Nothing happens
.then(value => console.log(2)) // Nothing happens
.catch(reason => console.log(reason)) // prints "fail"
๐ .catch
, .then
with onReject callback returns a promise. If you don't reject promise again, it will be fulfilled:
Promise.reject('fail')
.catch(reason => console.log(reason)) // prints "fail", returns `undefined`
.then(value => console.log(value)) // prints `undefined`
.catch(reason => console.log('fail 2')) // Nothing happens, promise is resolved
It's similar to try{}catch(e) {}
blocks. If you get into catch(e) {}
block you have to re-throw an error, if you want to handle it later. The same works with promises.
๐ Instead of error
event, unhandled promise rejections create unhandledrejection
event
globalThis.addEventListener("unhandledrejection", (event) => {
console.warn(`unhandledrejection: ${event.reason}`);
});
Promise.reject('test');
// Would print: "unhandledrejection: test"
๐ Throw in microtask code would reject the promise:
Promise.resolve(1)
.then(value => {throw value + 1})
.catch(reason => console.log(reason)) // prints 2
Alternatively you can return Promise.reject:
Promise.resolve(1)
.then(value => Promise.reject(value + 1))
.catch(reason => console.log(reason)) // prints 2
When you resolve a promise, resolving it with Promise.reject would also reject it:
new Promise(resolve => resolve(Promise.reject(1)))
.then(value => Promise.reject(2))
.catch(reason => console.log(reason)) // prints 1
Utility methods
๐ Promise.all allows you to await before all the promises change their state to fulfilled, or at least one promise gets rejected
const a = Promise.resolve(1);
const b = new Promise((resolve) => {
setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');
Promise.all([a, b, c]).then(console.log); // [1, 'foo', 'bar']
If any of the promises gets rejected, Promise.all will be rejected too:
const a = Promise.reject(1); // Now we reject the promise
const b = new Promise((resolve) => {
setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');
Promise.all([a, b, c])
.then(console.log) // Nothing
.catch(console.log); // 1
๐ Promise.allSettled awaits all promises to change their state. It returns an array with values and the promise statuses
const a = Promise.reject(1); // rejected promise
// 2 resolved promises
const b = new Promise((resolve) => {
setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');
Promise.allSettled([a, b, c]).then(console.log);
// [
// {status: 'rejected', reason: 1}
// {status: 'fulfilled', value: 'foo'}
// {status: 'fulfilled', value: 'bar'}
// ]
๐ Promise.race() method returns a promise that fulfills or rejects as soon as one of the promises fulfills or rejects with the value or reason from that promise
const a = Promise.reject(1); // rejected promise
// 2 resolved promises
const b = new Promise((resolve) => {
setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');
Promise.race([a, b, c])
.then(console.log) // nothing, as the first promise `a` is rejected
.catch(console.log) // 1
๐ If you put several already fulfilled or rejected promises to the Promise.race, Promise.race will depend on the order of the elements.
So if we change Promise.race([a,b,c])
from the first example, to Promise.race([b,c,a])
, the returned promise will be fulfilled with 'bar' value:
const a = Promise.reject(1); // rejected promise
// 2 resolved promises
const b = new Promise((resolve) => {
setTimeout(() => resolve('foo'), 1000);
});
const c = Promise.resolve('bar');
// We put 'a' at the very end
Promise.race([b, c, a])
.then(console.log) // 'bar'
.catch(console.log) // nothing
You can use this trick to test if the promise you have is already resolved or fulfilled.
Summing it up, this article covers base promise mechanisms and execution details. The following articles will cover concurrent and sequential execution, garbage collection, and some of the experiments on top of thenable objects.
Top comments (1)
Man, I just love your content!=)