JavaScript Concurrency: Promises Resolving to Promises
Welcome to the Promises Resolving to Promises lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
So far, we've always seen the
Promise.resolvefunction create fulfilled promises.>
Promise.resolve(5);Async Result:
{fulfilled: 5}If that's all the
resolvefunction does, why not call itPromise.fulfillinstead?There's an important reason for this naming choice. As you might suspect,
Promise.resolvedoes more than just fulfill promises. In JavaScript, promises can be resolved with other promises. For example, if we havepromise1, we can create another promise like this:>
const promise2 = Promise.resolve(promise1);We might expect this to create a promise that contains a promise. But in JavaScript there's no such thing! Instead, the promises are "flattened".
When
promise1fulfills,promise2will also fulfill with the same value. Likewise for rejection: ifpromise1rejects,promise2will reject with the same reason.>
const promise1 = Promise.resolve(5);const promise2 = Promise.resolve(promise1);promise2;Async Result:
{fulfilled: 5}>
const promise1 = Promise.reject(new Error('404 not found'));const promise2 = Promise.resolve(promise1);promise2;Async Result:
{rejected: 'Error: 404 not found'}Now we can answer the question that we started with: why isn't
Promise.resolvenamedPromise.fulfill? One answer is: becausePromise.resolvecan create rejected promises too!We can resolve with a promise via
Promise.resolve, via thenew Promiseconstructor, or via athencall. Resolving to promises works identically in all of those places.>
const promise1 = new Promise(resolve => resolve(6));const promise2 = new Promise(resolve => resolve(promise1));promise2;Async Result:
{fulfilled: 6}>
const promise1 = Promise.resolve().then(() => 7);Promise.resolve().then(() => promise1);Async Result:
{fulfilled: 7}A promise created in this way works like any other promise we've seen so far. For example, we can add a
thencallback to it, and thethencallback will see the value inside as usual.>
const promise1 = Promise.resolve(5).then(n => n + 1);Promise.resolve().then(() => promise1).then(n => n + 2);Async Result:
{fulfilled: 8}This has important consequences! For example,
thencallbacks can have entire chains ofthens inside. As long as thethencallback returns a promise, it will be flattened. That is, it'll only return the value of the promise, rather than the entire promise.Be careful with the next example; it's tricky. The third code block is combining values from
promise1andpromise2.>
const promise1 = Promise.resolve(5).then(n => n + 1);const promise2 = Promise.resolve(10);promise1.then(n1 => {return promise2.then(n2 => n1 + n2);});Async Result:
{fulfilled: 16}The fact that nested promises flatten reflects how they're used in practice. Let's imagine an alternate universe where promises don't work this way. In this universe, returning a promise inside the
thencallback results in something like{fulfilled: {fulfilled: 7}}: a promise nested inside another promise. But in our universe, the promises get flattened into{fulfilled: 7}.The practical reason for this is that the real-world problems, such as network requests, writes and reads to and from disks,
setTimeouts, etc. all compose nicely with promises that resolve to promises.For example, we can use
new Promisetogether withsetTimeoutto introduce a delay inside athencallback. Ifthens couldn't return promises then there would be no straightforward way to do this!>
Promise.resolve().then(() => {console.log('first then');}).then(() => {console.log('second then');return new Promise(resolve => setTimeout(resolve, 1000));}).then(() => {console.log('third then');});async console outputLet's take a close look at what happened above.
- Our promise resolved and called the first
then, which logged'first then' - The second
thenran and logged'second then' - The second
thencreated and returned anew Promise. But that promise won't fulfill untilsetTimeoutcallsresolve, which will take 1000 ms. - At about the 1000 ms mark, the third
thenran and logged'third then'.
- Our promise resolved and called the first
We can always pass a value to the
resolvecallback, even when calling it inside of a timer. The value we pass will flow into the nextthenin the usual way.>
Promise.resolve(5).then(n => {return new Promise(resolve => {setTimeout(() => resolve(n + 1), 1000);});}).then(n => {return n * 2;});Async Result:
{fulfilled: 12}That example marks a turning point in this course. It shows that we can pass values from
thentothenwhile also waiting for slow tasks to finish. Although we waited for asetTimeoutin that example, the same ideas apply when waiting for networks and disks. Here's a sketch of what this might look like when reading data from a database:>
function getAuthorNameForComment(commentId) {return database.getComment(commentId).then(comment => {// Query the database again , returning a promise that will resolve// to the user object.return database.getUser(comment.userId);}).then(user => {return user.name;});}Each
thencallback in that chain is its own little world. It takes an argument, does some asynchronous work, and returns a value (which may be a promise). The first twothens return the database queries' promises, so they'll fulfill with the value returned by the database. Finally, if a database query rejects due to some kind of failure, then that rejection will automatically flow through our entire promise chain.The idea of promises resolving to promises can be confusing at first. As with any other part of programming, it's a good idea to use simpler methods when possible. However, this is also a necessary part of any complex system that uses promises!