Callbacks in Asynchronous JavaScript
Table of Contents + −
In the previous lesson, we learned how to split code across files with import and export. Now let’s start working with asynchronous JavaScript, beginning with callbacks.
⏳ What Does “Asynchronous” Mean?
Most code runs synchronously, which means each line finishes before the next one starts. But some tasks do not finish right away. Loading data from a server, reading a file, or waiting for a timer all take time, and JavaScript does not stop and wait for them. These slow tasks are called asynchronous, because they finish later instead of immediately.
So you need a way to say, “run this code once the slow task is done.” That instruction is a callback: a function you hand to another function to be called back later.
| Type | Meaning | Example |
| Synchronous | Finishes right away, line by line | console.log("Hi") |
| Asynchronous | Finishes later, after some time | setTimeout(...) |
⏱️ setTimeout: The Simplest Async Example
The setTimeout function waits a set number of milliseconds, then runs a callback. It is the easiest way to see asynchronous behavior in action.
console.log("First");
setTimeout(() => { console.log("Second");}, 1000);
console.log("Third");Let’s walk through what JavaScript does with each line:
console.log("First")runs immediately and printsFirst.setTimeout(...)hands its callback to a timer and moves on without waiting. The1000means the callback is scheduled to run after 1000 milliseconds.console.log("Third")runs next and printsThird, because the synchronous lines do not wait for the timer.- Once the timer finishes, the callback runs and prints
Second.
So the output is First, then Third, then Second. JavaScript reads the file top to bottom, but the callback inside setTimeout is set aside and runs only after the timer finishes.
A delay of 0 still waits
Even setTimeout(fn, 0) runs the callback after the current code finishes. The
callback always waits its turn, no matter how small the delay.
📦 A Practical Example: Loading Data
Real apps load data from somewhere, and that takes time. Here is a simulated data load using setTimeout to stand in for a slow request. The callback runs once the data is ready.
function loadUser(callback) { console.log("Loading user...");
setTimeout(() => { const user = { id: 1, name: "Alex" }; callback(user); }, 1500);}
loadUser((user) => { console.log("Loaded:", user.name); // Loaded: Alex});Let’s trace how the data reaches the callback:
loadUseraccepts acallbackparameter, which is the function to run once the user is ready.- It prints
Loading user...right away to show the work has started. setTimeoutwaits 1500 milliseconds, then builds theuserobject and passes it intocallback(user).- The call to
loadUserhands in an arrow function, and that function receives theuserand prints its name.
The loadUser function does not return the user directly, because the user is not ready yet. Instead it calls the callback with the data once the timer finishes, so any code that needs the user must live inside that callback.
🌋 The Problem: Callback Hell
A single callback is easy to read. The trouble starts when one async task depends on another, and that one depends on a third. Each step nests inside the callback before it, and the code drifts to the right in a shape often called the pyramid of doom.
loadUser((user) => { loadPosts(user.id, (posts) => { loadComments(posts[0].id, (comments) => { loadAuthors(comments[0].id, (authors) => { console.log(authors); // buried deep inside }); }); });});Each line waits on the one above it, which is what forces the deep nesting:
loadUserruns first and gives us auser.- Only once we have the
usercan we callloadPosts(user.id, ...), so it nests inside the first callback. loadCommentsneeds a post id, so it nests inside the posts callback.loadAuthorsneeds a comment id, so it nests one level deeper still.- The final
console.log(authors)ends up buried far to the right.
This nesting is called callback hell. It is hard to read, hard to handle errors in, and hard to change. The next lesson introduces Promises, which flatten this pyramid into a clean chain.
Callbacks are not bad
Callbacks themselves are fine, and you will use them everywhere. The problem is only the deep nesting. Promises and async/await keep callbacks readable.
⚠️ Common Mistakes to Avoid
| Mistake | Problem | Solution |
| Assuming async code runs in order | Code after setTimeout runs before the callback | Put dependent code inside the callback |
| Deeply nested callbacks | The pyramid becomes unreadable | Use Promises or named functions |
| Ignoring errors in callbacks | A failed task fails silently | Pass an error argument and check it |
The error-first pattern is a common convention: the callback takes an error as its first argument, and you check it before using the result.
function loadData(callback) { setTimeout(() => { const error = null; const data = { value: 42 }; callback(error, data); }, 1000);}
loadData((error, data) => { if (error) { console.log("Something went wrong:", error); // ✅ handle the error return; } console.log("Got data:", data.value);});Let’s see how the error and the data flow through this callback:
loadDatacallscallback(error, data)with two arguments: an error first, then the result.- Here
errorisnull, which means nothing went wrong, anddataholds the value we want. - The callback checks
if (error)first. When an error is present, it prints a message and usesreturnto stop before touching the data. - When there is no error, it falls through and prints
data.value, which is42.
🔧 Try It Yourself!
- Write three
console.loglines with asetTimeoutin the middle, and predict the order before running it. - Create a
loadUserfunction that takes a callback and calls it with a user object after a delay. - Nest two callbacks so one async task runs after another finishes.
- Add an error-first callback that prints a message when an error is passed.
🧩 What You’ve Learned
- ✅ Asynchronous tasks finish later, not immediately
- ✅ A callback is a function passed in to run after an async task is done
- ✅
setTimeoutruns its callback after a delay, so synchronous code runs first - ✅ Deeply nested callbacks create callback hell, the pyramid of doom
- ✅ The error-first pattern lets callbacks report problems
🚀 What’s Next?
Now that you understand callbacks, we will learn a cleaner way to handle async work. Let’s continue to Promises.