Now that we understand what async JavaScript is and why it is needed, let's dive into how JavaScript handles asynchronous operations.


1️⃣ Callbacks – The Foundation of Async JavaScript

A callback function is a function passed as an argument to another function and executed later, typically after an asynchronous task is completed.

🔹 Example: Using Callbacks with setTimeout

function greet(name, callback) {
    console.log(`Hello, ${name}!`);
    callback();
}

function askQuestion() {
    console.log("How are you?");
}

greet("Salman", askQuestion);

🔍 Output:

Hello, Salman!
How are you?

➡️ Here, askQuestion is a callback function passed to greet, ensuring that it runs after greeting.

🔹 Example: Simulating Data Fetching

function fetchData(callback) {
    setTimeout(() => {
        console.log("Data fetched successfully!");
        callback();
    }, 2000);
}

function processData() {
    console.log("Processing the fetched data...");
}

fetchData(processData);

🔍 Output:

(Data fetches after 2 seconds)
Data fetched successfully!
Processing the fetched data...

➡️ processData is called after fetchData completes.


🚨 Callback Hell – The Problem with Callbacks

When multiple asynchronous tasks depend on each other, callbacks get deeply nested, making code hard to read and maintain.

Example: Nested Callbacks (Callback Hell)

function step1(callback) {
    setTimeout(() => {
        console.log("Step 1 completed");
        callback();
    }, 1000);
}

function step2(callback) {
    setTimeout(() => {
        console.log("Step 2 completed");
        callback();
    }, 1000);
}

function step3() {
    setTimeout(() => {
        console.log("Step 3 completed");
    }, 1000);
}

// Callback Hell
step1(() => {
    step2(() => {
        step3();
    });
});