Callback hell: Promises, Bluebird and ES7 (async/await) to the rescue

Karolina Nowocień
Tooploox
Published in
6 min readJun 16, 2017

I still remember the day when I discovered event-driving programming — what I felt then one might compare to the discovery that my bike will not always have three wheels. What is more, quite soon it turned out that the misuse of the new standard will most likely prevent anyone else from going through my code whatsoever.

This article will solve the problem of asynchronous control flow in JavaScript and will prevent you from falling into the callback hell. And trust me — you do not want to go there!

1. Callbacks

Let’s get this thing straight — there is nothing called a ‘callback’ in JavaScript language (there is a place called hell one might say though), it is just a way of naming, a convention. There are functions that returns the result at once and there are functions that use callbacks (they simply need more time to produce the result).

In JavaScript nesting callbacks is essential for all kind of long-running code like fetching data from a web API or different database operations. The core of Node.js uses callbacks for non-blocking actions. A significant part of the really bad nesting occurs when numerous callbacks are executed in one sequence:

callback-hell

2. What is a “callback hell” then?

callback-hell image

Writing Async JavaScript, or JavaScript that uses callbacks, is hard to do intuitively. In my (very own) experience a lot of code ends up looking like this:

Can you see this outline of the pyramid lurking in the background?

One of the biggest problems of callbacks is the connecting of different asynchronous activities one by one. Javascript let us have many anonymous functions (they can be, of course very powerful!) and we end up calling one after another to pass around values. This situation is known as callback hell or if you prefer — pyramid of doom ;)

It turns out to be a problem when the level of code nesting significantly trammels reading of the code (even after quite a few cups of coffee!) and error causes problems with the application…
When you work on a small amount of code you can use another JavaScript feature — you can name some of the functions and get rid of some of the nesting (but still — you do not want to do this):

3. thennables

In it’s ES6 and ES7 versions, JavaScript allows you to create a new type of object called a Promise.

Promises (or “thennables", as we interact with it by passing callback to its then function ) is a simple concept that makes life easier - they are an object that represents a value which will eventually be available. They are used for asynchronous or deferred computations and can be an alternative to the usual callback approach.

At their most basic, promises are a bit like event listeners except:

  • A promise can only succeed or fail once (and it cannot switch from success to failure or vice versa).
  • If a promise has succeeded or failed and you later add a success/failure callback, the correct callback will be called, even though the event took place earlier.

This is very useful for async success/failure, because you do not have to be interested in the time something became available, and more interested in responding to the outcome.

They also:

  • Flattened callbacks
  • Return values from asynchronous function
  • Throw and Catch exceptions

When using promises we take callbacks — especially nested callbacks where you want to perform a series of actions, each one after the resolution of the former- and “flatten out” the code required to do this.

Promises provide a cleaner and more effective way of representing asynchronous operations in javascript. They have a different syntax for achieving the same effect as callbacks. The advantage is better readability — we avoid all the nesting and it takes us definitely farther from hell! Here comes the example:

Compare this promise example…

…to a basic callback

And this is a promisified version of our callback hell example:

Promises can be used both on the browser (you can check the support here) or in Node.js apps.

You can probably see the benefits of using promises already. Most importantly, promises are amazing because most promise libraries (eg. Bluebird and Q) come with the promisify function that help you convert all your node style functions into promise returning functions.

However, there are also some disadvantages you should know of when it comes to promises:

  • You can’t cancel a Promise. Once you have a promise, the process that produces that promise’s resolution is already under way!*
  • Promises are fulfilled (resolved or rejected) with a single value. A promise conceptually represents a value over time so while you can represent composite values you can’t put multiple values in a promise.

*Unless you use Bluebird or another promise (CancelToken)

4. Bluebird

Promises are great for asynchronous functions that call their callback once. But promise libraries prevent you from ‘over promising’. As I have mentioned before there are some promise libraries that help you convert all your node style functions into promise returning functions. I will focus on Bluebird now as it has the incredibly useful functionality of enabling you to ‘promisfy’ modules which do not return promises. For example, to promisfy the fs module, simply require bluebird and a promisified version of fs.

5. DIY

Because of the fact that you can promisfy modules it is unlikely that you will have to create promises yourself very often. It is, however, still useful to know how. Creating a new promise provides you with the resolve and reject callback. Pass into each of these the appropriate information.

6. ES 7 (async/await)

Our code looks much better now, we have now a simple linear event flow — could it be better?

Apparently yes! There is still a lot of extra syntax we do not need and with ES 7 async and await keywords we will be allowed to get rid of it and write our JavaScript code as though it was synchronous!

With ES7 you can use all the commonly used (synchronous) JavaScript flow control and error handling mechanisms on async operations. Ever heard of if/else? :) I thought so - so what are you going to say about try/catch? There is no need to write libraries to make simple logical operations.

Back to promises, not only are they the basis of async/await, but you’ll still need to resort to them when implementing some more complex async IO scenarios (eg. running multiple operations in parallel). In general, await keyword takes a promise, waits for it’s value to be available, and then returns that value. The keyword async, in turn, makes the function always return a promise, and allows using await in its body.

Promise.all( )

If you think that using async and await gives you control over concurrent programming then try the Promise.all( ) method - it is even more powerful!
Let’s assume you want to perform two asynchronous computations in parallel - asyncFunc1( ) and asyncFunc2( ) :

Both functions — asyncFunc1( ) and asyncFunc2( ) are used without using then( ) chaining. It means neither more nor less, that they are both executed immediately. We have created two separate "threads" and once they are finished (with a result or an error) the execution is joined into a single "thread" (handleSuccess( ) or handleError( ).
As you can see that's quite a lot of code for a such simple operation! This approach involves way too much manual work which often results in generating errors.
Promise.all( ) is a built-in method. It takes an iretable argument - iretable such as an Array or String and When all the promises of the iretable argument have resolved (or if the argument contains no promises) it returns a single Promise.

Here is our previous example but this time we are using Promise.all( ) method:

We just wrote very simple yet powerful and easy to understand asynchronous code! Well done!

To sum up:

Async/await:

  • is a new way to write asynchronous code — earlier ways for dealing with asynchronous code are callbacks and promises.
  • it is (just like promises) non blocking.
  • is actually built on top of promises. It cannot be used with plain callbacks or node callbacks.
  • makes our code look and behave a little bit more like synchronous code — this makes it so powerful.

--

--