Hero image showing callback hell

This article is aimed at people starting out with asynchronous coding in javascript so we would keep things simple by avoiding big words, arrow functions, template literals etc.

Callbacks are one of the most used concepts of modern functional javascript and if you’ve ever used jQuery, chances are you’ve already used callbacks without even knowing (we will get back to it in a minute).

What the Heck are Callback Functions?

A callback function in its simplest terms is a function that is passed to another function, as a parameter. The callback function then gets executed inside the function where it is passed and the final result is returned to the caller.

1
// I'm sure you've seen a JQuery code snippet like this at some point in your life! The parameter we're passing to the `click` method here is a callback function.
2
3
$("button").click(function() {
4
alert('clicked on button`);
5
});

Simple right? Now let us implement a callback function to get scores on levelling up in an imaginary game.

1
// levelOne() is called a high-order function because
2
// it accepts another function as its parameter.
3
function levelOne(value, callback) {
4
var newScore = value + 5;
5
callback(newScore);
6
}
7
8
// Please note that it is not mandatory to reference the callback function (line #3) as `callback`, it is named so just for better understanding.
9
10
function startGame() {
11
var currentScore = 5;
12
console.log('Game Started! Current score is ' + currentScore);
13
// Here the second parameter we're passing to levelOne is the
14
// callback function, i.e., a function that gets passed as a parameter.
15
levelOne(currentScore, function (levelOneReturnedValue) {
16
console.log('Level One reached! New score is ' + levelOneReturnedValue);
17
});
18
}
19
20
startGame();

Once inside startGame() function, we call the levelOne() function with parameters as currentScore and our callback function().

When we call levelOne() inside startGame() function’s scope, in an asynchronous way, javascript executes the function levelOne() and the main thread keeps on going ahead with the remaining part of our code.

This means we can do all kind of operations like fetching data from an API, doing some math etc., everything which can be time-consuming and hence we won’t be blocking our main thread for it. Once the function(levelOne()) has done with its operations, it can execute the callback function we passed earlier.

This is an immensely useful feature of functional programming as callbacks lets us handle code asynchronously without us have to wait for a response. For example, you can make an ajax call to a slow server with a callback func. and completely forget about it and continue with your remaining code. Once that ajax call gets resolved, the callback function gets executed automatically.

But Callbacks can get nasty if there are multiple levels of callbacks to be executed in a chain. Let’s take the above example and add a few more levels to our game.

1
function levelOne(value, callback) {
2
var newScore = value + 5;
3
callback(newScore);
4
}
5
6
function levelTwo(value, callback) {
7
var newScore = value + 10;
8
callback(newScore);
9
}
10
11
function levelThree(value, callback) {
12
var newScore = value + 30;
13
callback(newScore);
14
}
15
16
// Note that it is not needed to reference the callback function as `callback` when we call levelOne(), levelTwo() or levelThree(), it can be named anything.
17
18
function startGame() {
19
var currentScore = 5;
20
console.log('Game Started! Current score is ' + currentScore);
21
22
levelOne(currentScore, function (levelOneReturnedValue) {
23
console.log('Level One reached! New score is ' + levelOneReturnedValue);
24
levelTwo(levelOneReturnedValue, function (levelTwoReturnedValue) {
25
console.log('Level Two reached! New score is ' + levelTwoReturnedValue);
26
levelThree(levelTwoReturnedValue, function (levelThreeReturnedValue) {
27
console.log('Level Three reached! New score is ' + levelThreeReturnedValue);
28
});
29
});
30
});
31
32
}
33
34
startGame();

Wait, what just happened? We added two new functions for level logic, levelTwo() and levelThree(). Inside levelOne’s callback(line #22), called levelTwo() function with a callback func. and levelOne’s callback’s result. And repeat the same thing for levelThree() function again.

callback meme

Now just imagine what this code will become if we had to implement the same logic for another 10 levels. Are you already panicking? Well, I am! As the number of nested callback functions increases, it becomes tougher to read your code and even harder to debug.

This is often affectionately known as a callback hell. Is there a way out of this callback hell?

I Promise there’s a better way

Javascript started supporting Promises from ES6. Promises are basically objects representing the eventual completion (or failure) of an asynchronous operation, and its resulting value.

1
// This is how a sample promise declaration looks like. The promise constructor takes one argument which is a callback with two parameters, `resolve` and `reject`. Do something within the callback, then call resolve if everything worked, otherwise call reject.
2
3
var promise = new Promise(function(resolve, reject) {
4
// do a thing or twenty
5
if (/* everything turned out fine */) {
6
resolve("Stuff worked!");
7
}
8
else {
9
reject(Error("It broke"));
10
}
11
});

Let us try to rewrite our callback hell example with promises now.

1
function levelOne(value) {
2
var promise, newScore = value + 5;
3
return promise = new Promise(function(resolve) {
4
resolve(newScore);
5
});
6
}
7
8
function levelTwo(value) {
9
var promise, newScore = value + 10;
10
return promise = new Promise(function(resolve) {
11
resolve(newScore);
12
});
13
}
14
15
function levelThree(value) {
16
var promise, newScore = value + 30;
17
return promise = new Promise(function(resolve) {
18
resolve(newScore);
19
});
20
}
21
22
var startGame = new Promise(function (resolve, reject) {
23
var currentScore = 5;
24
console.log('Game Started! Current score is ' + currentScore);
25
resolve(currentScore);
26
});
27
28
// The response from startGame is automatically passed on to the function inside the subsequent `then`
29
startGame.then(levelOne)
30
.then(function (result) {
31
// the value of `result` is the returned promise from levelOne function
32
console.log('You have reached Level One! New score is ' + result);
33
return result;
34
})
35
.then(levelTwo).then(function (result) {
36
console.log('You have reached Level Two! New score is ' + result);
37
return result;
38
})
39
.then(levelThree).then(function (result) {
40
console.log('You have reached Level Three! New score is ' + result);
41
});

We have re-wrote our level(One/Two/Three) functions to remove callbacks from the function param and instead of calling the callback function inside them, replaced with promises.

Once startGame is resolved, we can simply call a .then() method on it and handle the result. We can chain multiple promises one after another with .then() chaining.

This makes the whole code much more readable and easier to understand in terms of what is happening, and then what happens next and so on.

The deep reason why promises are often better is that they’re more composable, which roughly means that combining multiple promises “just works” while combining multiple callbacks often doesn’t.

Also when we have a single callback versus a single promise, it’s true there’s no significant difference. It’s when you have a zillion callbacks versus a zillion promises that the promise-based code tends to look much nicer.

Okay, we’ve escaped successfully from the callback hell and made our code much readable with promises. But what if I told you there’s a way to make it cleaner and more readable?

(a)Wait for it

Async- await is being supported in javascript since ECMA2017. They allow you to write promise-based code as if it were synchronous code, but without blocking the main thread. They make your asynchronous code less “clever” and more readable.

To be honest, async-awaits are nothing but syntactic sugar on top of promises but it makes asynchronous code look and behaves a little more like synchronous code, that’s precisely where it’s power lies.

If you use the async keyword before a function definition, you can then use await within the function. When you await a promise, the function is paused in a non-blocking way until the promise settles. If the promise fulfils, you get the value back. If the promise rejects, the rejected value is thrown.

Let us see now how our game logic looks once we rewrite it with async-awaits!

1
function levelOne(value) {
2
var promise, newScore = value + 5;
3
return promise = new Promise(function(resolve) {
4
resolve(newScore);
5
});
6
}
7
8
function levelTwo(value) {
9
var promise, newScore = value + 10;
10
return promise = new Promise(function(resolve) {
11
resolve(newScore);
12
});
13
}
14
15
function levelThree(value) {
16
var promise, newScore = value + 30;
17
return promise = new Promise(function(resolve) {
18
resolve(newScore);
19
});
20
}
21
22
// the async keyword tells the javascript engine that any function inside this function having the keyword await, should be treated as asynchronous code and should continue executing only once that function resolves or fails.
23
async function startGame() {
24
var currentScore = 5;
25
console.log(`Game Started! Current score is ${currentScore}`);
26
27
currentScore = await levelOne(currentScore);
28
console.log(`You have reached Level One! New score is ${currentScore}`);
29
30
currentScore = await levelTwo(currentScore);
31
console.log(`You have reached Level Two! New score is ${currentScore}`);
32
33
currentScore = await levelThree(currentScore);
34
console.log(`You have reached Level Three! New score is ${currentScore}`);
35
}
36
37
startGame();

Immediately our code becomes much more readable but there’s more to Async-await.

Error handling is one of the top features of Async-await which stands out. Finally we can handle both synchronous and asynchronous errors with the same construct with try and catches which was a pain with promises without duplicating try-catch blocks.

The next best improvement from good old promise world is code debugging. When we write arrow function based promises, we can’t set breakpoints inside our arrow functions so debugging is tough at times. But with async-awaits, debugging is just like how you would do a synchronous piece of code.

I’m sure that by now you have a better understanding of asynchronous programming in javascript. If you have a question, let me know below. If you found this helpful, give me a shoutout on Twitter!

Happy Coding!