Sitemap

CallBack hell

5 min readNov 18, 2024

--

Press enter or click to view image in full size

Introduction

In JavaScript, asynchronous programming allows us to handle operations like API calls, file reading, or timers without blocking the execution of other code. While powerful, it comes with challenges. A common way to handle asynchronous operations is through callback functions — functions passed as arguments and executed later.

However, as asynchronous logic grows in complexity, callbacks can lead to deeply nested and difficult-to-read code, often referred to as “callback hell” or the “pyramid of doom”. Here’s an example to illustrate:

first the code execution will be top to bottom

function task1()
{
console.log("task 1 running");
}

function task2()
{
console.log("task 2 running");
}

function task3()
{
console.log("task 3 running");
}

function task4()
{
console.log("task 4 running");
}

now the output will be as expected one function after the other

task 1 running
task 2 running
task 3 running
task 4 running

what if we introduced asynchronous programming to these functions

function task1()
{
setTimeout(() => {console.log("task 1 running")}, 2000);
}

function task2()
{
setTimeout(() => {console.log("task 2 running")}, 1500);
}

function task3()
{
setTimeout(() => {console.log("task 3 running")}, 3000);
}

function task4()
{
setTimeout(() => {console.log("task 4 running")}, 1000);
}

task1();
task2();
task3();
task4();

the tasks now will be compleated in this order:

task 4 running
task 2 running
task 1 running
task 3 running

and if we wanted them to be ordered we cave to introdue callback functions

function task1(callback)
{
setTimeout(() => {console.log("task 1 running"), callback()}, 2000);
}

function task2(callback)
{
setTimeout(() => {console.log("task 2 running"), callback()}, 1500);
}

function task3(callback)
{
setTimeout(() => {console.log("task 3 running"), callback()}, 3000);
}

function task4(callback)
{
setTimeout(() => {console.log("task 4 running"), callback()}, 1000);
}

task1(() => {
task2(() => {
task3(() => {
task4(() => {
console.log("finished");
});
});
});
});

the output will be ordered

task 1 running
task 2 running
task 3 running
task 4 running
finished

now let’s imaging if we have more tasks ordering them will be a hastle. The structure becomes increasingly unreadable as more callbacks are nested, Handling errors in each layer adds to the complexity.

Over time, JavaScript introduced new solutions to address this issue, such as Promises and async/await, making asynchronous code easier to write and maintain. Here’s a glimpse of how these approaches solve callback hell.

PROMISES:

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It allows you to work with asynchronous code in a more readable way, avoiding callback hell.

Get B.R.O.L.Y’s stories in your inbox

Join Medium for free to get updates from this writer.

A promise can be in one of the following states:

  • Pending: The operation is still running.
  • Fulfilled: The operation completed successfully.
  • Rejected: The operation failed.

Syntax

A promise is created using the new Promise() constructor, which takes a function as an argument. This function has two parameters:

  • resolve(): Called when the operation is successful.
  • reject(): Called when the operation fails.
let myPromise = new Promise((resolve, reject) => {
let success = true;

if (success) {
resolve("The operation was successful!");
} else {
reject("The operation failed.");
}
});

Consuming Promises

Once a promise is created, you can consume it using .then(), .catch(), and .finally():

  • .then() is called when the promise is fulfilled.
  • .catch() is called when the promise is rejected.
  • .finally() runs regardless of the outcome.
myPromise
.then((result) => {
console.log(result); // "The operation was successful!"
})
.catch((error) => {
console.log(error); // "The operation failed."
})
.finally(() => {
console.log("This runs no matter what.");
});

now let’s rewrite the first example using promises

function task1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("task 1 running");
resolve();
}, 2000);
});
}

function task2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("task 2 running");
resolve();
}, 1500);
});
}

function task3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("task 3 running");
resolve();
}, 3000);
});
}

function task4() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("task 4 running");
resolve();
}, 1000);
});
}

// Execute the tasks sequentially
task1()
.then(() => task2())
.then(() => task3())
.then(() => task4())
.then(() => {
console.log("finished");
});

Async/Await

The introduction of async/await in ES2017 (ES8) revolutionized asynchronous programming in JavaScript by making asynchronous code look and behave like synchronous code. It built on top of Promises, providing a cleaner and more readable syntax for handling asynchronous operations.

Async/await eliminates the need for chaining .then() methods or nesting callbacks, making error handling more straightforward and debugging easier.

  • async Function: Declares a function as asynchronous. It implicitly returns a promise.
  • await Keyword: Pauses the execution of an async function until the promise resolves or rejects. It can only be used inside async functions.

now let’s write the code in Async/await

function task1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("task 1 running");
resolve();
}, 2000);
});
}

function task2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("task 2 running");
resolve();
}, 1500);
});
}

function task3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("task 3 running");
resolve();
}, 3000);
});
}

function task4() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("task 4 running");
resolve();
}, 1000);
});
}

async function runTasks() {
await task1();
await task2();
await task3();
await task4();
console.log("finished");
}

// Run the tasks
runTasks();
  • Under the Hood: Async functions return a promise. When await is used, it pauses the execution of the async function until the promise resolves or rejects.
  • Non-blocking: Despite the “synchronous-like” appearance, async/await is non-blocking. Other operations can continue running while awaiting a promise.

Conclusion

Asynchronous programming is essential for building responsive and efficient JavaScript applications, but it can become challenging without the right tools. Callback hell, with its deeply nested structure, made asynchronous code difficult to read, maintain, and debug. The introduction of Promises provided a much-needed improvement by enabling chaining and better error handling.

However, async/await took asynchronous programming to the next level by offering a synchronous-like syntax that is intuitive, clean, and easy to debug. It not only eliminates the pitfalls of callback hell but also simplifies complex workflows, making JavaScript code more readable and maintainable.

In modern JavaScript development, async/await has become the standard for handling asynchronous tasks, thanks to its readability and efficiency. By adopting this approach and following best practices, developers can write robust, scalable, and error-resistant code.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

this is it for now. have a good day, and don’t forget to follow me on Medium and GitHub.

--

--

B.R.O.L.Y
B.R.O.L.Y

Written by B.R.O.L.Y

These are some of my notes that I think are worth publishing. Whether they help you depends on the path you take in your life.