Avoiding Callback Hell

As a Front End Engineer I’m always looking for an opportunity to dive into some javascript. We are currently in the process of migrating the existing Gousto web front end into a set of React.js components, transpiling ES6 with Babel and leveraging Redux data stores to manage state. Some very cool stuff.

Many of our API’s have been built as microservices. Recently, I had the task of inheriting one of these microservices, a legacy Node.js codebase. It’s always great working with a service that is brand new to you, but nothing quite prepares you for the feeling that hits you when cloning the git repository, locating the primary app entry point only to be greeted with:

Hopefully not that bad, but we’ve all been there. You want to write a small chunk of code that is going to head off and perform some asynchronous operation while your simple application carries on with it’s day job. Just pop a quick callback in and you might just have add, committed and pushed in time for an early lunch.

Unfortunately you don’t work for an agency, and unmaintainable code stays with you until it’s either A) rewritten, B) scrapped entirely, or worse, C) contributed to by 6 other developers who can’t escape your simple callback that seems to have ballooned into an unreadable, unmaintainable, spaghetti-mess of confused control flow.

Javascript callbacks are relatively simple. They behave much in the same way as callbacks in any other language. And because most developers are fairly well versed with the concept of callbacks, it can initially appear to be a very sensible design decision when writing an API. And if you’ve never experienced the spaghetti-callback-mess described above before, feel free to stop reading and go and search Youtube for cat videos


Chances are you’re still here, as “callback hell” is one of the most stumbled upon problems with asynchronous js. So what alternatives does javascript provide us to handle these situations better? A few, actually.

We could start writing listeners into our functions, and trigger events as and when we need. This is a fairly clean option, but there is still a potential for listeners being bound and events being triggered in very separate parts of your application. Nice separation of concerns, but if you’ve ever said “Why can’t I find the function that was called when that event was triggered?” you’ll recognise why this is problematic.

More recently, flow control libraries, such as async have made excellent progress in helping us write more readable and maintainable code, but there is still a lot of work to do here in order to handle errors gracefully.

Enter Promises.


Promises

Promises aren’t particularly new. In fact, even jQuery provided introduced promises into their slightly problematic implementation in v1.6 back in 2011. While implementation varies somewhat across languages & libraries, the concept itself is fairly simple…

Every promise starts out in a pending state. From here, it can either become fulfilled (with a return value from your async operation), or rejected (with an error from your async operation).

Let’s say we have an API that exposes a set of async functions that return promises

Promise-based API Consumption

1
2
3
4
5
6
7
8
9
10
11
12
asyncCall1()
.then(function() {
return asyncCall2();
}).then(function() {
return asyncCall3();
}).then(function() {
return asyncCall4();
}).catch(function(err) {
console.log("Threw an error: " + err);
}).then(function() {
console.log("All done!");
});

Callback-based API Consumption

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
try {
asyncCall1(err, data, function() {
if (err) {
console.log("Threw an error: " + err);
}
asyncCall2(err, data, function() {
if (err) {
console.log("Threw an error: " + err);
}
asyncCall3(err, data, function() {
if (err) {
console.log("Threw an error: " + err);
}
asyncCall4(err, data, function() {
if (err) {
console.log("Threw an error: " + err);
}
console.log("All done!");
});
});
});
});
} catch(err) {
console.log("Threw an error: " + err);
}

There is a few problems with the callback style approach that are mitigated with promises:

  1. Even with just 4 async calls the nesting is quickly getting out of control.
  2. We have to be very careful about errors, they could be thrown from any of our calls, and we should be prepared to handle them.
  3. Debugging your application flow becomes very difficult and it can easily confuse what state your application is in at any moment in time

Arguably one of the most difficult aspects of using asynchronous calls in any language is following application flow. As such, Promises allow us to write our application code in a synchronous style, without losing the benefit of async processing. Coupled with clear and concise error handling, Promises allow us to write clean, clear and maintainable code, something you’re likely aiming for if you’ve ever found yourself in callback hell

Not all APIs yet provide a promise implementation (though many do), but luckily there is a handful of great libraries that provide polyfills over existing ‘nodeback’ APIs.

The example above uses Promise.prototype.then() to chain the promises, ensuring they are fulfilled subsequently giving a more synchronous flow. But what about when we want to handle an API where we know there may be a race condition and as such only want to execute the promise that returns first? We have Promise.race() for that. Or how about do something when ALL of these promises have resolved? Promise.all().

So next time you’re consuming or writing an API, think about leveraging the power of promises to make your and your fellow developers lives easier.

Gareth
Senior Software Engineer

Excellent Resources

There is a swathe of great resources out there on promises, but the following give you most of what you need to make promises work for you.

The open standard for promise implementation
https://promisesaplus.com

Promise implementations that adhere to the above standard in your favourite languages.
https://promisesaplus.com/implementations

Promise patterns
https://www.promisejs.org/patterns/

Trusty mozilla docs
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise