Academic Block

JavaScript Callbacks Get Certified

A callback is a function passed into another function as an argument, to be called at a later point — either immediately, or once some task finishes.

Callbacks are one of the most fundamental patterns in JavaScript, since functions are treated as values that can be passed around just like numbers or strings.

In this lesson, you will learn how synchronous and asynchronous callbacks work, the error-first callback convention, and a common pitfall known as “callback hell.”

What Is a Callback?

A callback lets one function delegate part of its behavior to another function, decided by whoever calls it.

What this code does: A callback is a function passed as an argument to be called later. processPayment charges the amount, then calls onSuccess with that amount so the success message can be printed.

function processPayment(amount, onSuccess) {
    console.log("Charging $" + amount + "...");
    onSuccess(amount);
}
processPayment(50, function (amount) {
    console.log("Payment of $" + amount + " was successful!");
});

Here, processPayment doesn’t need to know exactly what happens after a successful charge — it just calls whatever function it was given.

Synchronous Callbacks

A synchronous callback runs immediately, as part of the same step-by-step execution. Many built-in array methods rely on synchronous callbacks.

What this code does: Array methods like forEach take a synchronous callback. The function runs once for each city in the list and logs it immediately.

const cities = ["Tokyo", "Cairo", "Lima", "Oslo"];
cities.forEach(function (city) {
    console.log("Visiting:", city);
});

Another common example is passing a custom comparison function to sort().

What this code does: sort uses a callback to decide order. Returning a - b sorts numbers from smallest to largest; the sorted array is then printed.

const distances = [42, 7, 19, 3, 56];
distances.sort(function (a, b) {
    return a - b;
});
console.log(distances);

Asynchronous Callbacks

An asynchronous callback is called later, after some operation completes — such as a timer finishing, a file loading, or a network request resolving. The rest of the code keeps running in the meantime.

What this code does: An asynchronous callback runs later. setTimeout schedules the message for 3 seconds later, while the next console.log runs right away—showing that the timer does not block.

console.log("Starting timer...");
setTimeout(function () {
    console.log("3 seconds have passed!");
}, 3000);
console.log("This runs before the timer finishes.");

Notice the order of the output: the last console.log runs before the setTimeout callback, because JavaScript doesn’t pause and wait for the timer.

Callbacks That Receive Parameters

The function that accepts a callback decides what arguments to pass to it when the callback is eventually called.

What this code does: You can write your own functions that accept callbacks. fetchWeather “fetches” a temperature and passes city and temperature into the callback for display.

function fetchWeather(city, callback) {
    const fakeTemperature = 24;
    callback(city, fakeTemperature);
}
fetchWeather("Barcelona", function (city, temperature) {
    console.log(city + ": " + temperature + "°C");
});

The Error-First Callback Pattern

A common convention, especially in Node.js-style code, is the error-first callback: the callback’s first parameter is reserved for an error (or null if there wasn’t one), and the actual result comes after.

What this code does: Error-first callbacks put the error in the first parameter. If the user is found, err is null and the age is used; otherwise the error message is logged.

function readUserAge(userId, callback) {
    const users = { 101: 28, 102: 34 };
    if (!users[userId]) {
        callback(new Error("User not found"), null);
        return;
    }
    callback(null, users[userId]);
}
readUserAge(101, function (err, age) {
    if (err) {
        console.log("Error:", err.message);
        return;
    }
    console.log("Age:", age);
});
readUserAge(999, function (err, age) {
    if (err) {
        console.log("Error:", err.message);
        return;
    }
    console.log("Age:", age);
});

Checking err first, before using the result, is a widely followed convention for this callback style.

Named Callbacks vs. Inline Anonymous Callbacks

A callback can be defined separately and passed by name, or written inline as an anonymous function. Naming a callback can make code more reusable and easier to test.

What this code does: Named functions can be passed as callbacks too. isLongWord is reused by filter to keep only words longer than 5 characters.

function isLongWord(word) {
    return word.length > 5;
}
const words = ["cat", "elephant", "sun", "mountain"];
const longWords = words.filter(isLongWord);
console.log(longWords);

Passing isLongWord by name works because filter() simply calls whatever function it’s given for each element.

Practical Example

Let’s use a callback to run custom logic after simulating a file upload.

What this code does: Combining async timing with the error-first pattern. After a short delay, the callback receives either an error or a success message depending on the file extension.

function uploadFile(fileName, onComplete) {
    console.log("Uploading " + fileName + "...");
    setTimeout(function () {
        const success = fileName.endsWith(".jpg") || fileName.endsWith(".png");
        if (success) {
            onComplete(null, fileName + " uploaded successfully.");
        } else {
            onComplete(new Error("Unsupported file type."), null);
        }
    }, 1000);
}
uploadFile("photo.png", function (err, message) {
    if (err) {
        console.log("Error:", err.message);
        return;
    }
    console.log(message);
});

Common Mistakes With Callbacks

  • Calling the callback function with parentheses when passing it, such as doSomething(callback()) instead of doSomething(callback), which runs it immediately instead of passing it.
  • Assuming an asynchronous callback runs before the code written after it.
  • Forgetting to check the error argument first in an error-first callback.
  • Nesting too many callbacks, making the code hard to follow — a sign it’s time to consider Promises or async/await.
  • Forgetting to actually call the callback inside the outer function, so nothing ever happens.

Best Practices for Callbacks

  • Always pass the callback function itself, not the result of calling it, unless that’s intentional.
  • Follow the error-first convention consistently if you’re writing your own asynchronous functions.
  • Give callbacks clear, descriptive names when they’re defined separately, rather than always using anonymous functions.
  • Keep nesting shallow — extract deeply nested callbacks into named functions, or consider using Promises instead.
  • Document what arguments a callback will receive, especially for reusable functions that others might call.

Quick Summary

  • A callback is a function passed as an argument to another function, to be called later.
  • Synchronous callbacks run immediately, like those used in forEach() and sort().
  • Asynchronous callbacks run after some operation completes, such as a timer or network request.
  • The error-first callback pattern reserves the first parameter for an error, checked before using the result.
  • Deeply nested callbacks are known as “callback hell,” and are often replaced with Promises or async/await.

Try It Yourself

Modify the code below. Try changing the delay, or adding a second console.log after the setTimeout call to see the execution order for yourself.

What this code does: Practice example: notice the execution order. “Order placed” and “Preparing next order” run first; “Order shipped” appears after the delay. Try changing the timeout or adding another log.

console.log("Order placed.");
setTimeout(function () {
    console.log("Order shipped!");
}, 2000);
console.log("Preparing next order...");
Tip: If you’re passing a callback and it doesn’t seem to run at all, double-check that you passed the function itself, not the result of calling it. It’s one of the most common typos when working with callbacks.

Understanding callbacks is essential before moving on to Promises, async/await, and more advanced JavaScript concepts.

🧪 Test Your JavaScript Code

Edit the code on the left and click “Run Code” to see the console output on the right.

📝 JavaScript Code
👁 Console Output

Click “Run Code” to see the output here.

Ctrl + Enter to run code Esc to close