Breaking and taming the Promise chain in JS

Before a .then() chain brings you back to the callback hell

Edoardo Nosotti
May 20, 2019 · 4 min read
Image for post
Image for post
Photo by Victor Rodriguez on Unsplash

You have probably read a lot about Promises and how they can help tidying up your code, replacing the old callbacks. Yet, when your code has to execute a number of operations, you might as well end up in a .then() chain that looks pretty much like the “callback hell” you were trying to escape from.

You also probably know about Promise.all(). It helps tidying up Promise chains, but what if you need to pass return variables across the chainedPromises? You might need to resort to workarounds that make your code more complex and less readable.

There are better ways to implement Promise chains. Let’s take a piece of ugly, old-school code and improve on that.

Driving a car, the wrong way

Take a look at this code (…I dare you!):

'use strict'function doAction(action) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`${action}: done.`)
}, 1000)
})
}
console.log('Getting ready to drive the car...')// Welcome to the "callback hell", the place where
// all bad programmers go ;)
// This is an example of how NOT to write JS code...
var driveCar = doAction('Unlock the car')
.then(result => {
console.log(result)
return doAction('Open the car door')
.then(result => {
console.log(result)
return doAction('Get on board')
.then(result => {
console.log(result)
return doAction('Insert key')
.then(result => {
console.log(result)
return doAction('Turn key')
.then(result => {
console.log(result)
return doAction('Release brake')
.then(result => {
console.log(result)
return doAction('Engaging gear')
.then(result => {
console.log(result)
console.log('Godspeed!')
})
})
})
})
})
})
})

It looks horrible, messy and is hardly maintainable.

Image for post
Image for post

A slightly better approach

The same code, rewritten usingPromise.all():

'use strict'function doAction(action) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`${action}: done.`)
}, 1000)
})
}
console.log('Getting ready to ride the car...')var unlockCar = doAction('Unlock the car')var openCarDoor = doAction('Open the car door')var getOnBoard = doAction('Get on board')var insertKey = doAction('Insert key')var turnKey = doAction('Turn key')var releaseBrake = doAction('Release brake')var engageGear = doAction('Engaging gear')Promise.all([
unlockCar,
openCarDoor,
getOnBoard,
insertKey,
turnKey,
releaseBrake,
engageGear
]).then(result => {
console.log(result)
console.log('Godspeed!')
})

This look definitely better, it is easier to read and maintain. The problem with this code is that the results returned from each Promise become available when all of the Promises in the chain have been resolved. As I mentioned in the introduction, there are ways to work around that:

'use strict'// We will use this variable to store return values
// while the Promises in the chain are resolved
var results = [];
function doAction(action) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(results) // Updated at each invocation
results.push(`${action}: done.`)
resolve()
}, 1000)
})
}
console.log('Getting ready to ride the car...')var unlockCar = doAction('Unlock the car')var openCarDoor = doAction('Open the car door')var getOnBoard = doAction('Get on board')var insertKey = doAction('Insert key')var turnKey = doAction('Turn key')var releaseBrake = doAction('Release brake')var engageGear = doAction('Engaging gear')Promise.all([
unlockCar,
openCarDoor,
getOnBoard,
insertKey,
turnKey,
releaseBrake,
engageGear
]).then(result => {
console.log('Godspeed!')
})

This is a common approach, but I don’t like it either. Storing all return values in an array is error-prone, should for example the order of invocation of the Promises change. I could define individual variables for each return value and then populate them one by one, but this code would still look untidy to me:

var result1;
var result2;
var result3;
...
function promisifiedFunction1() {
// Set result1
}
function promisifiedFunction2() {
// Use result1 and set result2
}
function promisifiedFunction3() {
// Use result2 and set result3
}
...
Promise.all([
promisifiedFunction1(),
promisifiedFunction2(),
promisifiedFunction3(),
...
]).then(result => {
console.log('Completed!')
})
Image for post
Image for post

Finally taming the Promise chain

This is my favourite approach to individually invoke each Promise, keeping the code tidy and passing return values to the next Promise:

'use strict'function doAction(action) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`${action}: done.`)
}, 1000)
})
}
console.log('Getting ready to drive the car...')var unlockCar = doAction('Unlock the car')var openCarDoor = unlockCar.then(result => {
console.log(result) // Result from unlockCar
return doAction('Open the car door')
})
var getOnBoard = openCarDoor.then(result => {
console.log(result) // Result from openCarDoor
return doAction('Get on board')
})
var insertKey = getOnBoard.then(result => {
console.log(result) // Result from getOnBoard
return doAction('Insert key')
})
var turnKey = insertKey.then(result => {
console.log(result) // Result from insertKey
return doAction('Turn key')
})
var releaseBrake = turnKey.then(result => {
console.log(result) // Result from turnKey
return doAction('Release brake')
})
var engageGear = releaseBrake.then(result => {
console.log(result) // Result from releaseBrake
return doAction('Engaging gear')
})
engageGear.then(result => {
console.log(result) // Result from engageGear
console.log('Godspeed!')
})

Bonus: async/await

Updated on 2019–06–11

As Bob Myers kindly pointed out, I should have also mentioned async/await as an alternate approach to Promise chains. They still use Promises under the hood, but with a different syntax and try/catch support for error handling. Check them out and see if they fit your coding style better, they should be now supported by most browsers and servers, hence be safe to use.

Here are some useful resources to learn about async/await:

Have fun!

Source code for this article is available on GitHub.

RockedScience

Tutorials, tips and fast news on Cloud, DevOps and Code

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store