π€ Promise
A Promise is an object representing the eventual β
completion or βfailure of an asynchronous operation. It allows you to attach handlers (onfulfilled
, onrejected
) via .then()
to the object to deal with the eventual success value or failure reason.
Three Statesβ
A Promise
is in one of these 3 states:
- β³οΈ pending: initial state and it is neither fulfilled nor rejected.
- β fulfilled: meaning that the async operation was completed successfully.
- β rejected: meaning that the async operation failed.
A promise is said to be settled if it is either β fulfilled or βrejected, but not β³οΈpending (Being settled is not a state, just a linguistic convenience).
"resolved" promises are often equivalent to "fulfilled" promises.
Constructorβ
new Promise(executor)
Static Methodsβ
There are 4 static methods that take an iterable of promises and return a new promise.
Promise.all()
β
The Promise.all()
method takes an iterable of promises as input and returns a single Promise
:
- β
This returned
Promise
already fulfills, if theiterable
passed is empty. - β
This returned
Promise
asynchronously fulfills when all of the input promises fulfill, with an array of fulfilled values, in the order of the promises passed, regardless of completion order. - β This returned
Promise
asynchronously rejects immediately when any of the input promises rejects, with this first rejection reason. This is also known as "Fail-Fast".
Implementationβ
Promise.all
resolves synchronously if and only if the iterable
passed is empty []
. In other situations, the returned Promise
will be asynchronously fulfilled or asynchronously rejected.
const resolvedPromisesArray = [Promise.resolve(33), Promise.resolve(44)];
const p = Promise.all(resolvedPromisesArray);
// Immediately logging the value of p
console.log(p); // Promise { <state>: "pending" }
// Using setTimeout, we can execute code after the queue is empty
setTimeout(() => {
console.log("the queue is now empty");
console.log(p); // Promise { <state>: "fulfilled", <value>: Array[2] }
});
function all(promises) {
// `Promise.all` resolves synchronously if and only if the iterable passed is empty
if (promises.length === 0) return Promise.resolve(promises);
// return a single `Promise`
return new Promise((resolve, reject) => {
// fulfill with an array of fulfilled values
const result = [];
promises.forEach((promise, i) => {
if (promise instanceof Promise) {
promise.then(
(value) => {
// the fulfilled value is in the order of the promises passed
result[i] = value;
// fulfill the promise when all the promises fulfill
if (result.length === promises.length) resolve(result);
},
(reason) => {
// reject immediately, when any of the promises rejects
reject(reason);
}
);
} else {
// if the iterable contains non-promise values,
// they will still be included in the returned promise array
// as if the promise is fulfilled
result[i] = promise;
if (result.length === promises.length) resolve(result);
}
});
});
}
The implementation above does not satisfy the asynchronously fulfilled/rejected requirement.
Promise.allSettled()
β
The Promise.allSettled()
method takes an iterable
of promises as input and returns a single Promise
:
- β
This returned
Promise
already fulfills, if theiterable
passed is empty. - β
This returned
Promise
asynchronously fulfills when all of the input promises settle. - β
This returned
Promise
WILL NOT REJECT.
This method is used:
- when you have multiple asynchronous tasks that are not dependent on each other to complete.
- Or you want to know the result of each promise.
const promises = [
Promise.resolve(2),
new Promise((resolve) => setTimeout(() => resolve(233))),
Promise.reject(new Error("The reason is that I want it to fail")),
"niu b",
];
Promise.allSettled(promises).then((values) => console.log(values));
/*
[
{
"status": "fulfilled",
"value": 2
},
{
"status": "fulfilled",
"value": 233
},
{
"status": "rejected",
"reason": new Error("The reason is that I want it to fail")
},
{
"status": "fulfilled",
"value": "niu b"
}
]
*/
Promise.any()
β
The Promise.any()
method takes an iterable
of promises as input and returns a single Promise
:
- β This returned
Promise
synchronously rejects when the input array is empty[]
. - β
This returned
Promise
asynchronously fulfills when ANY of the input promises β fulfills. - β This returned
Promise
asynchronously rejects when ALL of the input promises β reject, with anAggregateError
containing an array of rejection reasons ine.errors
field.
This method is useful for returning the FIRST promise that fulfills. It short-circuits after a promise fulfills, so it does not wait for the other promises to complete once it finds one.
In short, Promise.any()
takes the first fulfilled Promise
.
Unlike Promise.all()
and Promise.allSettled()
, which returns an array, Promise.any()
only return one fulfilled value (assuming at least one promise fulfills).
Implementationβ
function any(promises) {
if (promises.length === 0)
return Promise.reject(new AggregateError("Empty input."));
const errors = [];
return new Promise((resolve, reject) => {
promises.forEach((promise) => {
promise
.then((value) => {
resolve(value);
})
.catch((reason) => {
errors.push(reason);
if (errors.length === promises.length)
reject(new AggregateError(errors, "All promises were rejected"));
});
});
});
}
Promise.race()
β
The Promise.race()
method takes an iterable
of promises as input and returns a single Promise
:
- β³οΈ This returned
Promise
remains pending βΎοΈ forever if theiterable
passed is empty[]
. - β
This returned
Promise
asynchronously fulfills if the first promise to settle is fulfilled. - β This returned
Promise
asynchronously rejects if the first promise to settle is rejected.
The method is used when you want the first async task to complete, but do not care about its eventual state (i.e. it can either fulfilled
or rejected
).
In short, Promise.race()
takes the first settled Promise
.
Unlike Promise.all()
and Promise.allSettled()
, which returns an array, Promise.race()
only return one fulfilled value (assuming the first promise to settle is fulfilled).
Promise.race()
is always asynchronous: it never settles synchronously, even when the input is empty []
.
Use Casesβ
Using Promise.race()
to Implement Request Timeoutβ
const data = Promise.race([
fetch("/api"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timed out")), 5000)
),
])
.then((res) => res.json())
.catch((err) => console.log(err));
- If the
data
promise fulfills, it will contain the data fetched from/api
. - Otherwise, it will reject if the
fetch
operation takes more than 5 seconds because it loses the race with thesetTimeout
timer.
Using Promise.race()
to Detect the State of a Promiseβ
function getPromiseState(promise) {
const obj = {};
return Promise.race([promise, obj])
.then((value) => (value === obj ? "pending" : "fulfilled"))
.catch(() => {
return "rejected";
});
}
adapted from MDN docs
- If the
promise
is pending, the second value,obj
, which is a non-promise, becomes the result of the race. - If the
promise
is already fulfilled, thevalue
becomes the fulfilled value of thepromise
. - If the
promise
is already rejected, theonrejected
callback will be invoked.
The getPromiseState
function still runs asynchronously, because there is no way to get a promise's value without .then
or await
, even when it is already settled.
Promise.resolve()
β
The Promise.resolve()
method can take three types of input.
- If the input is a
Promise
, that promise is returned. - If the input is a
thenable
,Promise.resolve()
will call thethen()
method with two callbacks it prepared. - Otherwise, returns a
Promise
fulfilled with the input. (i.e.Promise.resolve(5)
)
Resolving Another Promiseβ
const originalPromise = Promise.resolve(233);
const newPromise = Promise.resolve(originalPromise);
newPromise.then((v) => console.log(v)); // 233
console.log(originalPromise === newPromise); // true
Resolving Thenableβ
const obj = {
then(onfulfilled, onrejected) {
console.log("inside then method");
onfulfilled("6666");
},
};
const p = Promise.resolve(obj); // cast thenable to promise
console.log(p); // p is an instance of Promise
p.then((v) => console.log(v));
Promise.reject()
β
The Promise.reject()
method returns a Promise
object that is rejected with a given reason.
It is useful to make reason
an instance of Error
.
Instance Methodsβ
Promise.prototype.then()
β
The .then()
method returns a Promise
immediately, which allows for method chaining. It accepts two optional arguments which will be executed to handle the current promises' fulfillment or rejection.
-
onFulfilled
A Function asynchronously called if the current promise is fulfilled. This function has one argument, the fulfilled value.
If it is not a function, it is internally replaced with an identity function
x => x
which simply passes the fulfilled value forward. -
onRejected
A Function asynchronously called if the current promise is rejected. This function has one argument, the rejection reason.
If it is not a function, it is internally replaced with a thrower function
x => {throw x;}
.
Returned Promiseβ
The .then()
method returns a Promise
immediately. This new promise is always β³οΈ pending when returned, regardless of the current promise's state.
The behavior of the returned promise (call it p
) depends on the handler's (onFulfilled
, onRejected
) execution result, following a specific set of rules.
If the handler function:
- β
returns a value:
p
gets fulfilled with the returned value as its value,[[PromiseResult]] === value
. - β
does not return anything:
p
get fulfilled withundefined
,[[PromiseResult]] === undefined
. - β throws an error:
p
get rejected with the thrown error,[[PromiseResult]] === Error
. - returns an already settled promise:
p
gets settled with that promise's result as its[[PromiseResult]]
. - β³οΈ returns another pending promise:
p
will be fulfilled/rejected based on the eventual state of that promise.
Examplesβ
In this example, the return value of onRejected
fulfills the p
.
// the first rule, the handler returns a value
// `p` gets fulfilled with the returned value
const p = Promise.reject("no reason!").then(
() => "will not be invoked",
() => "invoke the onRejected handler"
);
p.then((value) => console.log("value is " + value));
// logs: value is invoke the onRejected handler
const p = Promise.reject("no reason!")
.then(() => "will not be invoked")
.catch(() => "invoke the onRejected handler");
p.then((value) => console.log("value is " + value));
In this example, the handler functions are non-function.
// .then(x => x)
Promise.resolve("resolved value").then(2333).then(console.log); // "resolved value"
// .then(x => x, x => {throw x})
Promise.reject("a reason").then(666, "a ha~").then(console.log, console.log); // a reason
Promise.prototype.catch()
β
The .catch()
methods returns a Promise
immediately and will be invoked if the current promise is rejected. It behaves the same as calling Promise.prototype.then(undefined, onRejected)
.
Calling obj.catch(onRejected)
internally calls obj.then(undefined, onRejected).
Examplesβ
Errors thrown inside setTimeout
cannot be caught by .catch
.
const p = new Promise((resolve, reject) => {
setTimeout(() => {
throw new Error("an Error that cannot be caught by .catch");
}, 1000);
});
p.catch((e) => console.log(e));
To solve this, you should call reject()
inside the setTimeout
.
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("I am a reason!!!"));
}, 1000);
});
p.catch((e) => {
console.error(e);
});
Errors thrown after calling resolve()
will be silenced.
const p3 = new Promise((resolve, reject) => {
resolve();
throw new Error("Silenced Exception!");
});
p3.catch((e) => {
console.error(e); // This is never called
});
Promise.prototype.finally()
β
The .finally()
method returns a Promise
immediately.
The main use case is to avoid repeated code in both the promise's .then()
and .catch()
handlers. It is useful if you want to do some processing or cleanup once the promise is settled, regardless of its outcome.
The behavior of the returned promise (call it p
) depends on the handler function, following 2 rules:
- β If the handler throws an error or returns a rejected promise,
p
will be rejected with that value. - Otherwise, the return value of the handler DOES NOT AFFECT the state of the original promise.
Examplesβ
// rule 1
Promise.reject(46).finally(() => {
throw "good reason";
}); // good reason
// rule 1
Promise.reject(46).finally(() => Promise.reject("nice reason")); // nice reason
// rule 2
Promise.resolve("yes").finally(() => 77); // "yes"
// rule 2 - the current promise is rejected
Promise.reject("a reason").finally(() => 233); // "a reason"