Skip to main content

Callbacks and Asynchronous JavaScript

Created: December 18, 2025 5 min read

Callbacks are functions passed to other functions to be executed later. They’re fundamental to asynchronous programming in JavaScript.

What is Asynchronous Programming?

Asynchronous code doesn’t execute line-by-line. It allows operations to run in the background:

// Synchronous - blocks execution
console.log("Start");
const result = heavyComputation(); // Waits for completion
console.log("End");

// Asynchronous - doesn't block
console.log("Start");
heavyComputationAsync(() => {
    console.log("Done"); // Runs later
});
console.log("End"); // Runs immediately

Callbacks

A callback is a function passed as an argument to another function:

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

function sayGoodbye() {
    console.log("Goodbye!");
}

greet("Alice", sayGoodbye);
// Output:
// Hello, Alice!
// Goodbye!

Callbacks with Parameters

function fetchUser(userId, callback) {
    // Simulate API call
    setTimeout(() => {
        const user = { id: userId, name: "Alice" };
        callback(user);
    }, 1000);
}

fetchUser(1, (user) => {
    console.log(user); // { id: 1, name: "Alice" }
});

Error Handling with Callbacks

function fetchData(url, onSuccess, onError) {
    setTimeout(() => {
        if (url) {
            onSuccess({ data: "Success" });
        } else {
            onError("URL is required");
        }
    }, 1000);
}

fetchData(
    "https://api.example.com",
    (data) => console.log(data),
    (error) => console.error(error)
);

Node.js Error-First Callbacks

function readFile(filename, callback) {
    // Error-first convention: (error, data)
    setTimeout(() => {
        if (filename) {
            callback(null, "File contents");
        } else {
            callback(new Error("Filename required"));
        }
    }, 1000);
}

readFile("file.txt", (error, data) => {
    if (error) {
        console.error(error);
    } else {
        console.log(data);
    }
});

Callback Hell (Pyramid of Doom)

Multiple nested callbacks become hard to read:

// Callback Hell
getUser(userId, (error, user) => {
    if (error) {
        console.error(error);
    } else {
        getOrders(user.id, (error, orders) => {
            if (error) {
                console.error(error);
            } else {
                getOrderDetails(orders[0].id, (error, details) => {
                    if (error) {
                        console.error(error);
                    } else {
                        console.log(details);
                    }
                });
            }
        });
    }
});

Array Callbacks

Array methods use callbacks:

const numbers = [1, 2, 3, 4, 5];

// forEach
numbers.forEach((num) => {
    console.log(num);
});

// map
const doubled = numbers.map((num) => num * 2);

// filter
const evens = numbers.filter((num) => num % 2 === 0);

// find
const first = numbers.find((num) => num > 3);

Event Callbacks

Event handlers are callbacks:

const button = document.getElementById("myButton");

button.addEventListener("click", (event) => {
    console.log("Button clicked!");
});

button.addEventListener("mouseover", (event) => {
    console.log("Mouse over button");
});

setTimeout and setInterval

Common asynchronous operations:

// setTimeout - execute once after delay
setTimeout(() => {
    console.log("Executed after 1 second");
}, 1000);

// setInterval - execute repeatedly
const intervalId = setInterval(() => {
    console.log("Executed every 1 second");
}, 1000);

// Clear interval
setTimeout(() => {
    clearInterval(intervalId);
}, 5000);

Practical Examples

Retry Logic

function retryOperation(operation, maxRetries = 3, callback) {
    let attempts = 0;
    
    function attempt() {
        attempts++;
        operation((error, result) => {
            if (error && attempts < maxRetries) {
                console.log(`Attempt ${attempts} failed, retrying...`);
                attempt();
            } else {
                callback(error, result);
            }
        });
    }
    
    attempt();
}

retryOperation(
    (cb) => {
        // Simulated operation
        if (Math.random() > 0.7) {
            cb(null, "Success");
        } else {
            cb(new Error("Failed"));
        }
    },
    3,
    (error, result) => {
        if (error) {
            console.error("All attempts failed");
        } else {
            console.log(result);
        }
    }
);

Waterfall Pattern

function waterfall(tasks, callback) {
    let index = 0;
    
    function next(error, result) {
        if (error) {
            return callback(error);
        }
        
        if (index >= tasks.length) {
            return callback(null, result);
        }
        
        const task = tasks[index++];
        task(result, next);
    }
    
    next(null);
}

waterfall([
    (data, callback) => {
        console.log("Step 1");
        callback(null, "result1");
    },
    (data, callback) => {
        console.log("Step 2:", data);
        callback(null, "result2");
    },
    (data, callback) => {
        console.log("Step 3:", data);
        callback(null, "final result");
    }
], (error, result) => {
    console.log("Done:", result);
});

Parallel Execution

function parallel(tasks, callback) {
    const results = [];
    let completed = 0;
    
    tasks.forEach((task, index) => {
        task((error, result) => {
            if (error) {
                return callback(error);
            }
            
            results[index] = result;
            completed++;
            
            if (completed === tasks.length) {
                callback(null, results);
            }
        });
    });
}

parallel([
    (cb) => setTimeout(() => cb(null, "result1"), 1000),
    (cb) => setTimeout(() => cb(null, "result2"), 500),
    (cb) => setTimeout(() => cb(null, "result3"), 1500)
], (error, results) => {
    console.log(results); // ["result1", "result2", "result3"]
});

Best Practices

Use Named Functions

// Good - clear intent
function handleSuccess(data) {
    console.log(data);
}

function handleError(error) {
    console.error(error);
}

fetchData(url, handleSuccess, handleError);

// Avoid - anonymous functions
fetchData(url, (data) => console.log(data), (error) => console.error(error));

Keep Callbacks Simple

// Good - simple callback
array.forEach((item) => {
    console.log(item);
});

// Avoid - complex logic in callback
array.forEach((item) => {
    if (item.active) {
        const processed = item.value * 2;
        const formatted = processed.toFixed(2);
        console.log(formatted);
    }
});

Use Promises or Async/Await

// Avoid - callback hell
getUser(id, (error, user) => {
    if (error) {
        handleError(error);
    } else {
        getOrders(user.id, (error, orders) => {
            if (error) {
                handleError(error);
            } else {
                console.log(orders);
            }
        });
    }
});

// Better - use promises
getUser(id)
    .then(user => getOrders(user.id))
    .then(orders => console.log(orders))
    .catch(error => handleError(error));

// Best - use async/await
async function getOrdersForUser(id) {
    try {
        const user = await getUser(id);
        const orders = await getOrders(user.id);
        console.log(orders);
    } catch (error) {
        handleError(error);
    }
}

Summary

  • Callback: function passed to another function
  • Asynchronous: code that doesn’t block execution
  • Callback hell: deeply nested callbacks (avoid with Promises/async-await)
  • Error-first: convention for Node.js callbacks
  • Array methods: use callbacks for iteration
  • Events: use callbacks for event handling
  • Best practice: use Promises or async/await instead of callbacks

Official Documentation

Next Steps

  1. Promises: Creation, Chaining, Resolution
  2. Async/Await: Modern Asynchronous Programming
  3. Fetch API: Making HTTP Requests

Resources

Comments

Share this article

Scan to read on mobile