We all know how to resolve an instance of Promise:
const promise = new Promise((resolve) => {const someValue = "some value";setTimeout(() => {resolve(someValue);}, 1000);});
After 1 second, the promise
will be resolved with someValue
.
But when we resolve a Promise instance with another Promise instance, what will happen?
Resolve with a Pending Promise Instance
First, let’s see what will happen when we resolve a Promise instance with another Promise instance that is pending.
const button = document.createElement("button");button.appendChild(document.createTextNode("test 1 (pending)"));document.body.appendChild(button);function someAsyncTask() {return new Promise((resolveInner) => {setTimeout(() => {console.log("the inner resolve start");resolveInner();console.log("the inner resolve end");},1000);})}button.addEventListener("click", () => {const outerPromise = new Promise((resolveOuter) => {const innerPromise = someAsyncTask();innerPromise.then(() => {console.log("the inner's then callback");});console.log("the outer resolve start");resolveOuter(innerPromise);console.log("the outer resolve end");});outerPromise.then(() => {console.log("the outer's then callback");});});
What’s the output to the console?
"the outer try to resolve""the outer resolved"(after about 1 second)"the inner try to resolve""inner resolved""the inner's then callback""the outer's then callback"
We can see, the outer’s then
callback is called after the pendingPromise
given to resolveOuter
.
This means, after the resolveOuter
function is called, the outerPromise
is still pending.
And then, only after the innerPromise
is resolved/fulfilled, the outerPromise
is resolved/fulfilled.
You may also notice, the innerPromise
’s then
callback is called before the outerPromise
’s then
callback, indicating that the inner one is scheduled into the microtask queue before the outer one.
const button = document.createElement("button");button.appendChild(document.createTextNode("test 1 (pending)"));document.body.appendChild(button);function someAsyncTask() {return new Promise((resolveInner) => {setTimeout(() => {-------This function is scheduled when setTimeout is called.
After 1 sec, it will be push to macrotask queue.
console.log("the inner resolve start");3
resolveInner();console.log("the inner resolve end");4
},1000);})}button.addEventListener("click", () => {const outerPromise = new Promise((resolveOuter) => {const innerPromise = someAsyncTask();.then(() => {-------This function will be pushed to microtask queue when resolveInner()
console.log("the inner's then callback");5
});console.log("the outer resolve start");1
resolveOuter(innerPromise);console.log("the outer resolve end");2
});outerPromise.then(() => {-------This function will be pushed to microtask queue when resolveInner()
But after the callback of innerPromise.then()
console.log("the outer's then callback");6
});});
Resolve with a Resolved Promise
Then, let’s see what will happen when we resolve a Promise instance with another Promise instance that is resolved.
const button = document.createElement("button");button.appendChild(document.createTextNode("test 2 (resolved)"));document.body.appendChild(button);button.addEventListener("click", () => {const outerPromise = new Promise((resolveOuter) => {const resolvedPromise = Promise.resolve();resolvedPromise.then(() => {console.log("the inner's then callback");});console.log("the outer try to resolve");resolveOuter(resolvedPromise);console.log("the outer resolve end");});outerPromise.then(() => {console.log("the outer's then callback");});});
What’s the output to the console?
"the outer try to resolve""the outer resolved""the inner's then callback""the outer's then callback"
Since the inner Promise instance is already resolved, the behavior here is the same as when we resolve with a non-Promise value.
Resolve with a Rejected Promise
const button = document.createElement("button");button.appendChild(document.createTextNode("test 3 (rejected)"));document.body.appendChild(button);button.addEventListener("click", () => {const outerPromise = new Promise((resolveOuter) => {const rejectedPromise = Promise.reject();rejectedPromise.then(() => {console.log("the inner's then callback");});console.log("the outer resolve start");resolveOuter(rejectedPromise);console.log("the outer resolved?");});outerPromise.then(() => {console.log("the outer's then callback");});});
What’s the output to the console?
"the outer try to resolve""the outer resolved?"
The resolve function is called, but the Promise instance is not resolved.
Instead, the outer Promise instance is actually rejected. We can use catch
method to examine it:
outerPromise.catch(() => {console.log("the outer is rejected");});
Conclusion
When we resolve a Promise instance with another Promise instance, the outer Promise instance’s state will be determined by the inner Promise instance’s state.
- If the inner Promise instance is pending, the outer Promise instance will be pending.
- If the inner Promise instance is resolved, the outer Promise instance will be resolved.
- If the inner Promise instance is rejected, the outer Promise instance will be rejected.
And the then
callback of the inner Promise instance will be called before the then
callback of the outer Promise instance.
However, this behavior is not common in real-world scenarios. Because, in most cases, we will use async
/await
to handle the Promise instance.
For example, the first pending example can be almost fully rewritten as:
const button = document.createElement("button");button.appendChild(document.createTextNode("test 4 (async/await)"));document.body.appendChild(button);function someAsyncTask() {return new Promise((resolveInner) => {setTimeout(() => {console.log("the inner resolve start");resolveInner();console.log("the inner resolve end");},1000);})}button.addEventListener("click", () => {const ourterPromise = async () => {await someAsyncTask().then(() => {console.log("the inner's then callback");});console.log("the outer's then callback");};outerPromise();});
The main difference here is that we don’t have the opportunity to time the execution of statements such as console.log("the outer resolve start");
or console.log("the outer resolve end")
in the first example. There’s no way to run the statements between when someAsyncTask
is called and when it’s returned promise is fulfilled.
However, the async
/await
method is more readable because its behavior is more intuitive.