JavaScript Callbacks vs Promises vs Async Await

Javascript Callbacks vs Promises vs Async Await

JavaScript is a powerful programming language with its ability for closure, first class functions, and many other features. JavaScript is often used for Asynchronous Programming, or programming in a style that uses callbacks. While powerful, this can lead to what many now refer to as Callback Hell. Callback hell is also affectionately referred to as the Pyramid Of Doom thanks to the many levels of indentation which make your code look like a difficult to read pyramid. Let’s review the evolution of asynchronous programming in JavaScript from callbacks to async/await as this will help us in our Node.js programming.

What are JavaScript Callbacks Used For?

Callback functions in JavaScript are functions that can be passed to other functions as a parameter. They are also known as higher-order functions and are called (or executed) inside the function they were passed to. A callback is a convention, not an actual thing in the JavaScript language. Callbacks are used when you need to do things in your code which result in I/O, or input/output operations. Things like talking to a database, downloading a photo, or writing a file to disk are examples of I/O operations. These are asynchronous operations. In other words, they may take some time, or happen in the future. Check out an example callback function.

Let’s look at a code example here.

What do you think will happen when this code is run? Let’s see.

express-rest $node index.js

TypeError: Cannot read property 'title' of undefined
    at Object. (C:nodeexpress-restindex.js:21:22)
    at Module._compile (module.js:643:30)
    at Object.Module._extensions..js (module.js:654:10)
    at Module.load (module.js:556:32)
    at tryModuleLoad (module.js:499:12)
    at Function.Module._load (module.js:491:3)
    at Function.Module.runMain (module.js:684:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

The application says it can not read a property of undefined. But why? JavaScript does not wait for the async operation to complete to keep moving forward through the code execution. It simply runs each line of the program with no stopping. In other words it is non blocking. The getBlogPost() function runs immediately and sets the value of blogpost to undefined. This is why callbacks are needed when programming JavaScript and Node. We can rewrite this example using a callback so we get the result we want.

js callback in action

This is more along the lines of what we are looking for. We start the program, and after 2 seconds we see the output on the screen.

Callback Hell

Let’s think of a situation that could possibly cause callback hell. Imagine in your application that you would like to fetch a user from the database. Once you have that user, you now want to get all of that user’s blog posts. Once you have the blog posts, you also want to get the comments from that blog post. In a synchronous approach this is pretty straight forward. In the asynchronous world, this can start to cause some problems. The highlighted code here is what we refer to as the Pyramid Of Doom! You could also call it callback hell, but pyramid of doom just sounds so killer, haha!

This code works, but it’s not that easy to read nor look at. Worse, if you keep indenting further levels of callbacks you certainly will get lost in the code, create bugs, and simply create anti patterns that are not good for you!
pyramid of doom still works

Reducing Callback Hell With Named Functions

JavaScript makes it easy to shoot yourself in the foot if you are not careful. One such feature of the language which helps you do this are anonymous functions. Getify is known for suggesting that you always name your functions and he has a point. Debugging anonymous functions is next to impossible. In any event, let’s modify our code so far to use named functions and see how that helps out readability. Here we refactor the code to use named functions. It’s better, but still not that great.

How JavaScript Promises Can Help

Promises hold the eventual result of an asynchronous operation. When an asynchronous operation completes, it can either result in a value or an error. The three states a promise can hold are Pending, Fulfilled, or Rejected. Here is a fictional example of using a promise to make an API request.

When the API request was successful (apiResponse = 1), all is good.
resolve promise

When the API request was unsuccessful (apiResponse = 0), the .catch of the promise chain displays an error.
reject promise

With this understanding, we can now replace asynchronous functions that take a callback, with a returned promise. Let’s see how.

Promises In Place Of Callbacks

In the example code we showed for callback hell, we have the pyramid of doom structure. In other words, we have that deeply nested problem that is hard to read. Here, we can modify the asynchronous functions to now return a promise. Since those async functions return a promise, we can then consume those promises using .then() and .catch() thereby eliminating the callback hell nested pyramid. Note the pyramid of doom is now commented out, and re written using a .then() chain right below. Also note the supporting asynchronous functions no longer accept a callback as a parameter since they are now returning a Promise instead. Hopefully it is clear that inside the returned promise is where the async code now goes(Reading a database, or making an API request).

Async Await Syntactic Sugar

Finally, you may consider using the newest solution to callback hell, and that would be async/await. The goal of async/await is to help software developers write asynchronous code as if it were synchronous code, or in a synchronous looking style. To demonstrate, here is the code for both the Promise based and Async/Await approach. Note the Promise approach is commented out.

It looks like the code works with the async/await method as well.
async await in nodejs

Some key things to understand here about async/await. You use the await keyword when you call a function that is returning a Promise. This allows you to assign the result to a constant or variable just like you would in a synchronous operation. For example the code const user = await getUser(1); looks very nice and synchronous, but the await keyword gives us an indication that the function is returning a promise. This now facilitates the ability to call getBlogPosts( passing in the user object we just got in the prior line of code. Anytime you see that await keyword before a function, you know it is returning a promise. The great thing though is that you can now write that code in a more synchronous looking style. The other takeaway is that since you are using await inside of a function, you need to use the async keyword on that function. Async and Await are built on top of Promises and are a way to have nicer syntax much like ES6 Classes are a nicer syntax for Prototypes and Constructor Functions. If writing asynchronous code makes your head spin, then maybe Async/Await are just what you need.

Javascript Callbacks vs Promises vs Async Await Summary

When you’re writing asynchronous code, it is easy to get yourself into trouble, especially if you’ve been writing synchronous code for a long time. The truth is, making use of callbacks in a clean and concise way is challenging. You may find yourself in the callback hell pyramid of doom faster than you can blink an eye. You can fight back against call back hell with these tips.