Welcome to part 4 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. Then in part 3, we saw how each call to then actually makes a new promise, and that those promises can be chained together. In this post, we’ll see how to apply what we’ve learned to simplify asynchronous coding challenges.

[more]

I hope you like nesting…

Have you ever seen (or written) code like this?

widgetSvc.getWidget(id).then(function(widget) {
    $ctrl.widget = widget;
    listingSvc.getProductListing(widget.listingId).then(function(listing) {
        $ctrl.price = listing.price;
        
        manufacturerSvc.getManufacturer(listing.manufacturer.Id).then(function(manufacturer) {
            $ctrl.manufacturerName = manufacturer.name;
        });
    })
})

Let the awesomeness of that block sink in: that’s three async calls, combined together in a horrid mass of nested code. And I’ve seen examples that were far, far worse than that in production over the years. There weren’t always better ways to deal with this. “Callback hell” was real, folks!

There’s a better way!

Fortunately, promises actually give us a better way to tackle this problem! In the previous post, we saw that we can chain then calls together, creating new promises that are resolved with whatever we return from our then callbacks.

We can build on that same concept, and return new promises that need to be resolved, and then chain off of these new promises. Let’s look at a simple example!

Let’s go ahead and create a promise that will resolve in the future…

const promise = new Promise(resolve => setTimeout(() => resolve(1), 2000));

And let’s hook up a then callback that returns another new promise that will be resolved in the future…

const childPromise = promise.then(result => {
    console.log(`First callback - ${new Date().getSeconds()}s: ${result}`);
    return new Promise(resolve => setTimeout(() => resolve(result+1), 2000));
});

And let’s attach a then callback to this new child promise that also returns a new promise…

const grandchildPromise = childPromise.then(result => {
    console.log(`Second callback - ${new Date().getSeconds()}s: ${result}`);
    return new Promise(resolve => setTimeout(() => resolve(result+1), 2000));
});

And for good measure, let’s add one last then callback…

grandchildPromise.then(result => {
    console.log(`Third callback - ${new Date().getSeconds()}s: ${result}`);
});

This chain of promises will give us the following output:

First callback - 24s: 1
Second callback - 26s: 2
Third callback - 28s: 3

The above example is actually more verbose than it needs to be. We could rewrite it more cleanly as:

const promise = new Promise(resolve => setTimeout(() => resolve(1), 2000));
promise.then(result => {
    console.log(`First callback - ${new Date().getSeconds()}s: ${result}`);
    return new Promise(resolve => setTimeout(() => resolve(result+1), 2000));
}).then(result => {
    console.log(`Second callback - ${new Date().getSeconds()}s: ${result}`);
    return new Promise(resolve => setTimeout(() => resolve(result+1), 2000));
}).then(result => {
    console.log(`Third callback - ${new Date().getSeconds()}s: ${result}`);
});

The output will be the same!

Putting it all together

Now that we understand what’s going on and how to chain promises that return promises, we can easily rewrite the original, nested sample above:

widgetSvc.getWidget(id).then(widget => {
    $ctrl.widget = widget;
    return listingSvc.getProductListing(widget.listingId);
}).then(listing => {
    $ctrl.price = listing.price;
    return manufacturerSvc.getManufacturer(listing.manufacturer.Id);
}).then(manufacturer => {
    $ctrl.manufacturerName = manufacturer.name;
});

All we did was change each of our then callbacks to return the next promise. The next then callback will be bound to the promise we returned, and it won’t execute until it’s been resolved!

Keep in mind that everything we’ve previously learned about promises still applies! We could make multiple promise-returning calls within one of our then callbacks, and combine those into a single result using Promise.all, like so:

widgetSvc.getWidget(id).then(widget => {
    $ctrl.widget = widget;
    return listingSvc.getProductListing(widget.listingId);
}).then(listing => {
    $ctrl.price = listing.price;
    return Promise.all([
        manufacturerSvc.getManufacturer(listing.manufacturer.Id),
        resellerSvc.getReseller(listing.reseller.Id)
    ]);
}).then((manufacturer, reseller) => {
    $ctrl.manufacturerName = manufacturer.name;
    $ctrl.resellerName = reseller.name;
});

That’s it for this installment of “Practical Promises,” but we’re not finished yet! In the next post, we’ll look at unwrapping things through Promises, and how we can leverage that concept to build cleaner APIs in our JavaScript code.