A callback is a function you pass to another function, to be called later when something finishes. You’ve already used them — .map(), .filter(), and .forEach() all take callbacks.
Copy
// You already know callbacksconst numbers = [1, 2, 3];numbers.forEach(function(num) { // This function is a callback console.log(num);});// Arrow function version (same thing)numbers.forEach(num => console.log(num));
For async operations, callbacks work the same way: “When this async task finishes, call this function with the result.”
Copy
// setTimeout uses a callbackconsole.log("Starting timer...");setTimeout(() => { console.log("Timer finished!"); // Called after 2 seconds}, 2000);console.log("Timer is running in the background...");// Output:// Starting timer...// Timer is running in the background...// Timer finished! ← 2 seconds later
Node.js standardized a pattern: the first argument to a callback is always the error (or null if no error).
Copy
function getUser(userId, callback) { setTimeout(() => { if (userId <= 0) { callback(new Error("Invalid user ID"), null); return; } const user = { id: userId, name: "Sarah Chen" }; callback(null, user); // null = no error }, 1000);}// UsagegetUser(1, function(error, user) { if (error) { console.error("Failed:", error.message); return; } console.log(user.name); // "Sarah Chen"});
The error-first pattern (callback(error, result)) is a convention, not a language feature. Node.js APIs follow this pattern, but browser APIs like fetch use Promises instead.
The real problem shows up when you need multiple async operations that depend on each other — get a user, then get their orders, then get the order details:
These are fine because they’re not being nested. The problem was using callbacks for sequential async operations.
You don’t need to master callbacks for async work. The point of this lesson is to understand why Promises and async/await were created. For actual async code, you’ll use async/await.