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!");
});
function processPayment(amount, onSuccess) { declares a function with two parameters. The second one, onSuccess, is expected to be a function: the callback. The function that receives it decides when to call it.console.log("Charging $" + amount + "..."); simulates charging the customer by printing a message. The output is Charging $50....onSuccess(amount); calls the function that was passed in, handing it the amount. processPayment does not know what that function does. That decision belongs to the caller.} ends the processPayment function body.processPayment(50, function (amount) { calls processPayment with two arguments: the number 50 and an anonymous function written inline. That inline function becomes the onSuccess callback.console.log("Payment of $" + amount + " was successful!"); is what the caller wants to happen after a successful charge. It runs when processPayment calls onSuccess(amount). The output is Payment of $50 was successful!.}); closes the callback’s body with }, closes the argument list with ) and ends the statement with ;. The complete output is Charging $50... followed by Payment of $50 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);
});
const cities = ["Tokyo", "Cairo", "Lima", "Oslo"]; creates an array of four city names.cities.forEach(function (city) { calls the array method forEach() and passes it an anonymous function as the callback. forEach() calls that function once for every item in the array, passing each item in as city. This is a synchronous callback: every call happens right now, before the next line of your program runs.console.log("Visiting:", city); runs four times, once for each city: Visiting: Tokyo, Visiting: Cairo, Visiting: Lima and Visiting: Oslo.}); closes the callback body, the call to forEach() and the statement.
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);
const distances = [42, 7, 19, 3, 56]; creates an array of numbers in no particular order.distances.sort(function (a, b) { calls sort() with a callback that compares two items, a and b. sort() calls it again and again, with different pairs, to decide the final order. This is another synchronous callback.return a - b; returns a number that tells sort() which item goes first. If the result is negative, a goes before b; if it is positive, b goes first; if it is 0, their order does not matter. So subtracting sorts the numbers from smallest to largest. (Without this callback, sort() would compare the numbers as text and give a wrong order.)}); closes the callback, the call to sort() and the statement. Note that sort() changes the original array.console.log(distances); prints the sorted array: 3, 7, 19, 42, 56.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.");
console.log("Starting timer..."); runs immediately and prints Starting timer....setTimeout(function () { asks the browser to run a function later. The function is the callback. setTimeout() does not wait: it only schedules the callback and returns at once, so the program moves on to the next line.console.log("3 seconds have passed!"); is the code that will run when the timer finishes, 3 seconds from now. This is an asynchronous callback.}, 3000); closes the callback body, and 3000 is the delay in milliseconds (3 seconds). The ); ends the setTimeout() call.console.log("This runs before the timer finishes."); runs immediately after line 4, long before the timer fires. The actual order of output is: Starting timer..., then This runs before the timer finishes., and 3 seconds later 3 seconds have passed!. Note for the Test My Code window: it only shows what is printed while your code is running. A timer message arrives later, so it appears in your browser’s developer console (press F12) instead of the output panel.
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");
});
function fetchWeather(city, callback) { declares a function with two parameters: the city and a callback function. The function will decide which arguments to give to the callback.const fakeTemperature = 24; stands in for a real weather lookup. A real function would get the temperature from a server.callback(city, fakeTemperature); calls the callback and passes two arguments: the city and the temperature. The callback must be written to accept them in the same order.} ends the function body.fetchWeather("Barcelona", function (city, temperature) { passes the city name and an inline callback. The callback’s parameters, city and temperature, receive the two values that fetchWeather passes: "Barcelona" and 24.console.log(city + ": " + temperature + "°C"); joins the pieces of text with +. The output is Barcelona: 24°C.}); closes the callback, the argument list and the statement.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);
});
function readUserAge(userId, callback) { declares a function that takes a user id and a callback. It will use the error-first convention: the callback’s first argument is always an error (or null), and the result comes second.const users = { 101: 28, 102: 34 }; creates an object that maps user ids to ages: user 101 is 28 and user 102 is 34.if (!users[userId]) { looks up the id in the object. If the user does not exist, the lookup gives undefined, and the ! (NOT) turns that into true, so the block runs.callback(new Error("User not found"), null); calls the callback with an Error object as the first argument and null as the result.return; ends the function so that the success call below does not run as well.} closes the if block.callback(null, users[userId]); is reached only when the user exists. It passes null as the error (meaning no error) and the age as the result.} ends the function body.readUserAge(101, function (err, age) { calls the function with an existing id and an inline callback. The callback’s two parameters are err and age, in the error-first order.if (err) { checks whether an error was passed. For user 101, err is null, which is falsy, so this block is skipped.console.log("Error:", err.message); would print the error’s message text. It is skipped for user 101.return; would end the callback after reporting the error, so the code below it is not used with a missing result.} closes the if block.console.log("Age:", age); runs because there was no error. The output is Age: 28.}); closes the callback, the argument list and the statement.readUserAge(999, function (err, age) { calls the function again, this time with an id that does not exist.if (err) { is now true, because readUserAge passed an Error object.console.log("Error:", err.message); prints the message text of the error: Error: User not found.return; ends the callback, so the line that prints the age is skipped.} closes the if block.console.log("Age:", age); is skipped for user 999 because of the return above.}); closes the callback and the statement. The complete output is Age: 28 and then Error: User not found.
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);
function isLongWord(word) { declares an ordinary named function with one parameter. It will be used as a callback, but it is defined separately and has a clear name.return word.length > 5; compares the number of characters in the word (word.length) with 5, and returns true for words longer than 5 characters and false otherwise.} ends the function body.const words = ["cat", "elephant", "sun", "mountain"]; creates an array of four words.words.filter(isLongWord) passes the function itself as the callback: only the name, without parentheses. Writing isLongWord() would run the function immediately instead. filter() calls it once for each word, and keeps the words for which it returns true. The result is stored in longWords.console.log(longWords); prints the filtered array: elephant (8 letters) and mountain (8 letters). The words cat and sun have 3 letters, so they were left out.
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);
});
function uploadFile(fileName, onComplete) { declares a function that takes a file name and a callback, onComplete, that will be called when the upload is done.console.log("Uploading " + fileName + "..."); runs immediately and prints Uploading photo.png....setTimeout(function () { pretends that the upload takes time. The function inside will run later, so the callback onComplete is called asynchronously.const success = fileName.endsWith(".jpg") || fileName.endsWith(".png"); uses the string method endsWith(), which returns true if the text ends with the given ending. The || (OR) operator makes the result true if the name ends with .jpg or .png. For "photo.png" it is true.if (success) { checks the result.onComplete(null, fileName + " uploaded successfully."); follows the error-first convention: null for the error, and a success message as the result.} else { starts the branch for unsupported file types.onComplete(new Error("Unsupported file type."), null); passes an Error object as the first argument, and null as the result. This is skipped for photo.png.} closes the else block.}, 1000); closes the timer’s function, sets the delay to 1000 milliseconds (1 second) and ends the setTimeout() call.} ends the uploadFile function. The function returns right away, without waiting for the timer.uploadFile("photo.png", function (err, message) { starts the upload and passes the callback. Its parameters follow the error-first order: err, then message.if (err) { is false for photo.png, because err is null.console.log("Error:", err.message); would print the error message. It is skipped here.return; would stop the callback after an error. It is skipped here.} closes the if block.console.log(message); runs after the one-second delay, and prints photo.png uploaded successfully.. Try changing the file name to "notes.txt" to see the error branch. Note for the Test My Code window: it only shows what is printed while your code is running. A timer message arrives later, so it appears in your browser’s developer console (press F12) instead of the output panel.}); closes the callback, the argument list and the statement. The first line of output is Uploading photo.png..., and one second later comes the success message.Common Mistakes With Callbacks
- Calling the callback function with parentheses when passing it, such as
doSomething(callback())instead ofdoSomething(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()andsort(). - 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...");
console.log("Order placed."); runs immediately and prints Order placed..setTimeout(function () { schedules a function to run later and moves on immediately, without waiting.console.log("Order shipped!"); is the callback’s code. It runs only when the timer finishes.}, 2000); sets the delay to 2000 milliseconds (2 seconds) and ends the setTimeout() call. Try changing this number.console.log("Preparing next order..."); runs right after the timer was scheduled, so it prints before Order shipped!. The order of the output is Order placed., Preparing next order... and, after 2 seconds, Order shipped!. Note for the Test My Code window: it only shows what is printed while your code is running. A timer message arrives later, so it appears in your browser’s developer console (press F12) instead of the output panel.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.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.