Execute Program

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.resolve function create fulfilled promises.

  • >
    Promise.resolve(5);
    Asynchronous Icon Async Result:
    {fulfilled: 5}Pass Icon
  • If that's all the resolve function does, why not call it Promise.fulfill instead?

  • There's an important reason for this naming choice. As you might suspect, Promise.resolve does more than just fulfill promises. In JavaScript, promises can be resolved with other promises. For example, if we have promise1, 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 promise1 fulfills, promise2 will also fulfill with the same value. Likewise for rejection: if promise1 rejects, promise2 will reject with the same reason.

  • >
    const promise1 = Promise.resolve(5);
    const promise2 = Promise.resolve(promise1);
    promise2;
    Asynchronous Icon Async Result:
    {fulfilled: 5}Pass Icon
  • >
    const promise1 = Promise.reject(new Error('404 not found'));
    const promise2 = Promise.resolve(promise1);
    promise2;
    Asynchronous Icon Async Result:
    {rejected: 'Error: 404 not found'}Pass Icon
  • Now we can answer the question that we started with: why isn't Promise.resolve named Promise.fulfill? One answer is: because Promise.resolve can create rejected promises too!

  • We can resolve with a promise via Promise.resolve, via the new Promise constructor, or via a then call. 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;
    Asynchronous Icon Async Result:
    {fulfilled: 6}Pass Icon
  • >
    const promise1 = Promise.resolve().then(() => 7);
    Promise.resolve()
    .then(() => promise1);
    Asynchronous Icon Async Result:
    {fulfilled: 7}Pass Icon
  • A promise created in this way works like any other promise we've seen so far. For example, we can add a then callback to it, and the then callback will see the value inside as usual.

  • >
    const promise1 = Promise.resolve(5)
    .then(n => n + 1);

    Promise.resolve()
    .then(() => promise1)
    .then(n => n + 2);
    Asynchronous Icon Async Result:
    {fulfilled: 8}Pass Icon
  • This has important consequences! For example, then callbacks can have entire chains of thens inside. As long as the then callback 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 promise1 and promise2.

  • >
    const promise1 = Promise.resolve(5)
    .then(n => n + 1);

    const promise2 = Promise.resolve(10);

    promise1.then(n1 => {
    return promise2.then(n2 => n1 + n2);
    });
    Asynchronous Icon Async Result:
    {fulfilled: 16}Pass Icon
  • 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 then callback 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 Promise together with setTimeout to introduce a delay inside a then callback. If thens 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');
    });
    Asynchronous Icon async console output
  • Let's take a close look at what happened above.

    1. Our promise resolved and called the first then, which logged 'first then'
    2. The second then ran and logged 'second then'
    3. The second then created and returned a new Promise. But that promise won't fulfill until setTimeout calls resolve, which will take 1000 ms.
    4. At about the 1000 ms mark, the third then ran and logged 'third then'.
  • We can always pass a value to the resolve callback, even when calling it inside of a timer. The value we pass will flow into the next then in the usual way.

  • >
    Promise.resolve(5)
    .then(n => {
    return new Promise(resolve => {
    setTimeout(() => resolve(n + 1), 1000);
    });
    })
    .then(n => {
    return n * 2;
    });
    Asynchronous Icon Async Result:
    {fulfilled: 12}Pass Icon
  • That example marks a turning point in this course. It shows that we can pass values from then to then while also waiting for slow tasks to finish. Although we waited for a setTimeout in 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 then callback 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 two thens 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!