What Will Happen if We Resolve a Promise Instance With Another Promise Instance?

Published on Updated on

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.