Execute Program

JavaScript Concurrency: Promise Then

Welcome to the Promise Then lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We've seen a way to write a sequential series of setTimeouts. (We can also do this with any other callback-based asynchronous functions, like setInterval, which we'll see later in this course.) Sequential setTimeouts become difficult to read and debug as we add more steps:

  • >
    setTimeout(() => {
    console.log('0.5 seconds have passed');
    setTimeout(() => {
    console.log('1.0 second has passed');
    setTimeout(() => {
    console.log('1.5 seconds have passed');
    }, 500);
    }, 500);
    }, 500);
  • That code is difficult to read because there's a lot of nesting. We can imagine a function that automatically waits for the previous timer to finish. We could call it then:

  • >
    setTimeout(() => { console.log('0.5 seconds have passed'); }, 500);
    then(() => { console.log('1.0 second has passed'); }, 500);
    then(() => { console.log('1.5 seconds have passed'); }, 500);
  • We can't write exactly that function. However, JavaScript's Promise feature gets close to that syntax. Promises allow us to chain asynchronous operations in a clean, composable way. They also help us to avoid the deep nesting that we saw above.

  • Let's temporarily forget about concurrency, and instead start with some very normal-looking JavaScript:

  • >
    let value = 5;
    value = value * 3;
    value = value + 1;
    value;
    Result:
    16Pass Icon
  • We'll transform this code to put each step in its own function, then chain them together. The code will remain synchronous, but this will help us understand what promises do.

  • First, we can rewrite the assignments as small functions. If we immediately call each function, they'll have the same effect as the assignments above.

  • >
    let value = 5;
    value = (n => n * 3)(value);
    value = (n => n + 1)(value);
    value;
    Result:
    16Pass Icon
  • That chain only involved two functions calls, but we can add more.

  • >
    let value = 5;
    value = (n => n * 3)(value);
    value = (n => n + 1)(value);
    value = (n => n * 2)(value);
    value;
    Result:
    32Pass Icon
  • There's a repeated pattern here: value = someFunction(value). We'll introduce a chain function to contain that pattern for us. Here's a first pass at that function:

  • >
    function chain(value) {
    return {
    value: value,
    };
    }

    chain(5);
    Result:
    {value: 5}Pass Icon
  • chain returns an object, so we can access that object's properties immediately using dot notation.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    chain(5).value;
    Result:
    5Pass Icon
  • Next, we add a then method, which takes our callback function (the thing we want to do to our value) as an argument. We'll pass the callback in like someChain.then(n => n * 3).

  • Our then method should return the new value: in this case, n * 3. Think about then like this: "whatever came before me happened, then this function is going to happen".

  • >
    function chain(value) {
    return {
    value: value,
    then: callback => {
    return callback(value);
    }
    };
    }
  • Note that just like value, then is a property on the object chain returns. Just as we accessed chain(5).value above, we can also do chain(5).then(callback) to call the callback function.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    chain(5)
    .then(n => n * 3);
    Result:
    15Pass Icon
  • Now, the final step: we change then's return value. Instead of returning a value like 5 directly, we wrap the return value in a new chain. Now the chain function lives up to its name: when we call .then, we get a new chain, and we can call .then on that, which gives us a new chain, etc. When we're done, we access .value

  • Here's the full chain function, followed by some example uses of it:

  • >
    function chain(value) {
    return {
    value: value,
    then: callback => {
    return chain(callback(value));
    }
    };
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    chain(5)
    .then(n => n * 3)
    .value;
    Result:
    15Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    chain(5)
    .then(n => n * 3)
    .then(n => n + 1)
    .value;
    Result:
    16Pass Icon
  • Our chain function is still synchronous because it always runs the callback functions right away, rather than deferring them until later. Still, it shows us the basic interface of promises: there's a way to create an object containing a value, and there's a way to add then callbacks that return new values.

  • Now we can switch to real promises, where the equivalent of chain(someValue) is Promise.resolve(someValue). For now, you can think of Promise.resolve as "create a promise that contains the given value". (We'll explore the subtle details of what "resolve" actually means in later lessons!)

  • >
    Promise.resolve(11)
    .then(n => n + 1)
    .then(n => n / 2)
    .then(n => console.log(n));
    Asynchronous Icon async console output
  • There are a total of four promises constructed in that example: the initial Promise.resolve call returns a promise, and each of the then calls also returns a promise. Just like our chain function, each promise has a then method. However, there's no value property for us to call. Instead, we can access the value directly inside the then function.

  • If you look at a promise in your browser's built-in console, you'll see something like this. (There might be some variation depending on your browser.)

  • >
    Promise.resolve(5);
    Result:
  • The "Promise" and "5" parts are easy: it's a promise with a 5 inside.

  • The "fulfilled" part means that the promise actually contains a value. Later, we'll see promises that are waiting for something else to happen ("pending") and promises where something has gone wrong ("rejected"). For now, all that matters is that every "fulfilled" promise contains a value.

  • We'll be working with promises often, so a quick note on how our interface treats them. Execute Program shows promises similarly to how web browsers do. However, unlike browsers, we need to let you type promises in as the answers to our code examples. The Promise {<fulfilled>: 5} syntax that we saw above would be awkward to type out. Instead, we format fulfilled promises as simple JavaScript objects.

  • >
    Promise.resolve(5);
    Asynchronous Icon Async Result:
  • >
    Promise.resolve(5)
    .then(n => n + 1);
    Asynchronous Icon Async Result:
    {fulfilled: 6}Pass Icon
  • Here's a code problem:

    Add another then call to wrap the promise's string value in an array.

    Promise.resolve(5)
    .then(n => n * 2)
    .then(n => n.toString())
    .then(n => [n]);
    Asynchronous Icon Async Result:
    Goal:
    {fulfilled: ['10']}
    Yours:
    {fulfilled: ['10']}Pass Icon