try-catch-FAIL

Failure is inevitable

NAVIGATION - SEARCH

Practical Promises in JavaScript - The Basics of Promise Chaining

Welcome to part 3 of my Practical Promises series! In part 1, we talked about what promises are and what they can be used for. In part 2, we started looking at how we can create promises. Today, we're going to look at how promises can be chained together.

What Happens If...

We've seen that we can register callbacks on a promise using then and catch. But what happens if we register multiple callbacks?

const promise = Promise.resolve(1);
promise.then(result => console.log(`I got the result: ${result}`));
promise.then(result => console.log(`And so did I: ${result}`));

Both of our callbacks will be executed!

OUTPUT:
I got the result: 1
And so did I: 1

You probably expected that. But did you know that when you call then you are actually making a new promise?? Here, let me prove it:

const promise = Promise.resolve(1);
const child1 = promise.then(result => {});
const child2 = promise.then(result => {});

console.log('Are they equal? ', child1 === child2 ? 'Yep!' : 'Nope!')
OUTPUT: 
Are they equal?  Nope!

This has some interesting implications. For example, if we were to chain multiple then calls together...

const promise = Promise.resolve(1);
promise.then(result => console.log(`I got the result: ${result}`))
       .then(result => console.log(`Here is what I got: ${result}`));

We see that the first callback is resolved with our value, but the second one is not!

OUTPUT:
I got the result: 1
Here is what I got: undefined

So what's going on? Well, our first then call actually creates a new promise, which is resolved with the value we return from our first callback (which, as we'll get in to later, is actually undefined in this case!) We can leverage that to orchestrate more complex behaviors and chain promises together.

Passing Data Between Promises

We've established that each call to then actually creates a new promise, and that whatever we return in our callback is used to resolve the new promise. That means we can do things like this:

const promise = Promise.resolve(1);
promise.then(result => {
            console.log(`I got the result: ${result}`);
            return result + 1;
        })
       .then(result => {
            console.log(`And so did I: ${result}`);
            return result + 1
       })
       .then(result => {
            console.log(`Me, too: ${result}`);
       });
OUTPUT:
I got the result: 1
And so did I: 2
Me, too: 3

Notice that I changed things up to use block-bodied arrow functions instead of the usual concise-bodied functions. That's because when we chain then calls in this way, we need to actually return something. If we had used our concise-bodied functions...

const promise = Promise.resolve(1);
promise.then(result => console.log(`I got the result: ${result}`))
       .then(result => console.log(`But I didn't: ${result}`))
       .then(result => console.log(`Me either: ${result}`));

We'd get this as output:

I got the result: 1
But I didn't: undefined
Me either: undefined

Are you confused yet? Well, you should be! This is one of the subtleties of JavaScript arrow-functions that trips up .NET developers like me. Let me rewrite this same example another way. Our concise-bodied functions are actually the same as this:

const promise = Promise.resolve(1);
promise.then(result => {
            return console.log(`I got the result: ${result}`);
        })
       .then(result => {
            return console.log(`And so did I: ${result}`);
       })
       .then(result => {
            return console.log(`Me, too: ${result}`);
       });

We're returning the result of console.log, which itself returns undefined, hence why we get undefined being passed through our chain of promises!

tl;dr

Here is the important takeaway from this post: each time we call then we're actually creating a new Promise. If we return a value from our promise, it is the same as creating a new promise with Promise.resolve(value).

Things get interesting when we actually return a new promise from within our then callbacks. And that's exactly what we'll talk about in the next post. 😃

About Matt Honeycutt...

Matt Honeycutt is a software architect specializing in ASP.NET web applications, particularly ASP.NET MVC. He has over a decade of experience in building (and testing!) web applications. He’s an avid practitioner of Test-Driven Development, creating both the SpecsFor and SpecsFor.Mvc frameworks.

He's also an author for Pluralsight, where he publishes courses on everything from web applications to testing!

blog comments powered by Disqus