How to escape Promise Hell

Unlike Callback Hell, Promise Hell is accidentally self inflicted. Promise Hell is just a lack a familiarity with Promises.

This article will show some Promise Hell scenarios and the steps out get of it. All code snippets are written ECMAScript 2015.


Nested.then(..) with linearly dependent Promises

A typical example is Promises that depend on each other. Let’s say we have 3 Promises that depend on each other linearly: fetchBook(), formatBook(book), and sendBookToPrinter(book). The naïve code is:

fetchBook()
.then((book) => {
return formatBook(book)
.then((book) => {
return sendBookToPrinter(book);
});
});

The nesting is unnecessary, which simplies the code to:

fetchBook()
.then((book) => {
return formatBook(book);
})
.then((book) => {
return sendBookToPrinter(book);
});

Since these are single liners, the curly braces and return can be omitted.

fetchBook()
.then((book) => formatBook(book))
.then((book) => sendBookToPrinter(book));

But then we notice we have an identity closure! We can just inline the function:

fetchBook()
.then(formatBook)
.then(sendBookToPrinter);

Nested .then(..) with independent Promises

In another scenario, we might have Promises that can be run independently. Let’s say we have 3 functions that return Promises: demandMoreDonuts(), greet(name), and closeGate(). The naïve code would be:

demandMoreDonuts()
.then(() => {
return greet('fred')
.then(() => {
return closeGate();
});
});

How to get out of this situation depends if we care that these Promises are run in serial or in parallel. If we don’t care about the order of the Promises, then this can be simplified to:

Promise.all([
demandMoreDonuts(),
greet('fred'),
closeGate()
]);

If we want to serialize the Promise order (say we want to abort early on error), then we can write this as:

demandMoreDonuts()
.then(() => greet('fred'))
.then(closeGate);

Nested.then(..) with multiple dependent Promises

This situation is what most people get hung up on with Promises. In this example, let’s say we have 4 Promises: connectDatabase(), findAllBooks(database), getCurrentUser(database), and pickTopRecommendation(books, user). The naïve code is:

connectDatabase()
.then((database) => {
return findAllBooks(database)
.then((books) => {
return getCurrentUser(database)
.then((user) => {
return pickTopRecommendation(books, user);
});
});
});

Naïvely we will think this is the best we can do. How else can we get a reference to books and user at the same time? This code also has other issues, such as when we call getCurrentUser(database), we have books unnecessarily in scope. The solution is to understand Promises can be held in a reference. We can extract the common bits out.

const databasePromise = connectDatabase();
const booksPromise = databasePromise
.then(findAllBooks);
const userPromise = databasePromise
.then(getCurrentUser);
Promise.all([
booksPromise,
userPromise
])
.then((values) => {
const books = values[0];
const user = values[1];
return pickTopRecommentations(books, user);
});

But wait, doesn’t this mean we connect to the database twice? We call .then(..) on databasePromise twice! This is a key thing to understand about Promises, they are only ever resolved once per creation. This means you can call .then(..) as many times as you want, and you’ll only ever get one database connection. This is not the case if you call connectDatabase() multiple times.

The extraction of the values out of promises is a bit ugly. It can be simplifed using the spread operator, but this is only available in Node.js 5.x or greater.

Promise.all([
booksPromise,
userPromise
])
.then((values) => pickTopRecommentations(...values));

… or with destructuring with Node.js 6.x or greater.

Promise.all([
booksPromise,
userPromise
])
.then(([books, user]) => pickTopRecommentations(books, user));

Alternatively it can be simplified with Ramda’s apply.

Promise.all([
booksPromise,
userPromise
])
.then(R.apply(pickTopRecommentations));

Optional work with dependent Promises

Adding options to a program can always be tricky. Let’s say you want to load some data, and have 3 options to attach more information. While loading a user, you can optionally attach their favourite food, which school they attended, and the school’s faculty. The faculty is dependent on whether the school was loaded.

The naïve implementation is to make everything linear since it is tricky to handle the options correctly.

const databasePromise = connectDatabase();
return databasePromise
.then(getCurrentUser)
.then((user) => {
if (attachFavouriteFood) {
return getFood(user.favouriteFoodId)
.then((food) => {
user.food = food;
return user;
});
}
return user;
.then((user) => {
if (attachSchool) {
return getSchool(user.schoolId)
.then((school) => {
user.school = school;
if (attachFaculty) {
return getUsers(school.facultyIds)
.then((faculty) => {
user.school.faculty = faculty;
return user;
});
}
return user;
});
}
return user;
});

There are several problems with this code. As we already mentioned, we are loading the food, then the school, when it can be loaded in parallel. Second of all we have to remember to return user at every branch. Very error prone.

We can attempt to parallelize the load of the food and school using a promises array.

const databasePromise = connectDatabase();
return databasePromise
.then(getCurrentUser)
.then((user) => {
const promises = [];
if (attachFavouriteFood) {
promises.push(getFood(user.favouriteFoodId)
.then((food) => {
user.food = food;
}));
}
if (attachSchool) {
promises.push(getSchool(user.schoolId)
.then((school) => {
user.school = school;
if (attachFaculty) {
return getUsers(school.facultyIds)
.then((faculty) => {
user.school.faculty = faculty;
});
}
}));
}
return Promises.all(promises)
.then(() => {
return user;
});
});

While this is now correct and faster, it feels imperative. Note the very last line, we Promise.all so we know when user is finished being mangled.

The best solution to this problem I’ve seen is to separate the loading from the final assignment to the user. Fork, then join.

const databasePromise = connectDatabase();
return databasePromise
.then(getCurrentUser)
.then((user) => {
const foodPromise = attachFavouriteFood
? getFood(user.favouriteFoodId)
: undefined;
const schoolPromise = attachSchool
? getSchool(user.schoolId)
: undefined;
const facultyPromise = attachSchool && attachFaculty
? schoolPromise
.then((school) => getUsers(school.facultyIds))
: undefined;
return Promise.all([foodPromise, schoolPromise, facultyPromise])
.then(([food, school, faculty]) => {
if (food) {
user.food = food;
}
if (school) {
user.school = school;
if (faculty) {
user.school.faculty = faculty;
}
}
return user;
});
});

Promise.all will just pass through non-promises, so using undefined is safe. This will scale better to any set of dependencies.


Much more complex examples can be formed using a combination of these scenarios. I hope this will help somebody escape Promise Hell. If you have more specific examples that I have not covered, feel free to tweet me.