JavaScript • Asynchronous JavaScript

Callback Functions — Understand callback functions and common callback patterns (intro)

Learn how passing functions as arguments lets JavaScript handle tasks that finish at different times.

JavaScript emblem with code brackets, braces, and a code file.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

Early computer programs ran one instruction after another in a strict sequence, like following a recipe step by step. If any step took a long time — for instance, reading a file from a disk — the entire program would freeze and wait. This was fine for simple batch programs, but as software grew more interactive, developers needed a way to say, "Go do this task, and when you're done, run this other piece of code." That idea became the callback function, one of the oldest and most important patterns in programming.

JavaScript was created in 1995 by Brendan Eich at Netscape specifically for web browsers. Browsers are single-threaded environments, meaning they can only do one thing at a time. If JavaScript waited around for a network request to finish, the entire web page would become unresponsive — no scrolling, no clicking, nothing. Callbacks solved this problem elegantly: you hand the browser a function and say, "Call this back when the data arrives." The browser goes off to fetch the data, and your page stays responsive.

1958 • Function Pointers in LISP

LISP introduced the idea of treating functions as data that could be passed to other functions, laying the theoretical foundation for callbacks.

1995 • JavaScript Is Born

Brendan Eich creates JavaScript for Netscape Navigator, building callbacks into the language's DNA for handling browser events like button clicks.

1999 • XMLHttpRequest & AJAX

Microsoft introduces XMLHttpRequest, and callback-driven AJAX calls let web pages fetch data without reloading — a game-changer for web apps.

2009 • Node.js Launches

Ryan Dahl releases Node.js, bringing JavaScript to servers. Its entire I/O model is built on callbacks, making them essential knowledge for all JS developers.

2015 • Promises & Beyond

ES6 introduces Promises as a cleaner alternative to deeply nested callbacks, but callbacks remain the underlying mechanism that Promises build upon.

Understanding callbacks is essential because nearly every modern JavaScript pattern — Promises, async/await, event listeners, and array methods — is built on top of callbacks. The key question this lesson addresses: How do we write code that responds to events and delayed operations without freezing our programs?

SECTION 2

Core Principles & Definitions

A callback function is simply a function that you pass as an argument to another function, with the expectation that it will be "called back" (executed) at a later time. In JavaScript, functions are first-class citizens, which means they can be stored in variables, passed as arguments, and returned from other functions — just like numbers or strings. This capability is what makes callbacks possible.

1

First-Class Functions

In JavaScript, functions are values. You can assign a function to a variable, pass it to another function as an argument, or return it from a function. This is the foundation that makes callbacks work.
2

Higher-Order Functions

A higher-order function is any function that accepts another function as a parameter or returns a function. Examples include setTimeout, .forEach(), and .map().
3

Synchronous vs. Asynchronous

A synchronous callback runs immediately during the execution of the higher-order function. An asynchronous callback is scheduled to run later, after some operation completes.
4

The Event Loop

JavaScript uses a single-threaded event loop to manage asynchronous callbacks. When an async task finishes, its callback is placed in a queue, and the event loop runs it once the main thread is free.
5

Error-First Convention

In Node.js, callbacks follow the error-first pattern: the first parameter is always an error object (or null if no error), and the second is the result data.
✦

KEY TAKEAWAY

Think of a callback like leaving your phone number at a restaurant when there's a long wait. Instead of standing at the door blocking everyone (synchronous waiting), you hand over your number (the callback) and go do other things. The restaurant calls you back when your table is ready. That's exactly what JavaScript does: it hands off a function to be called later, freeing the main thread to keep working.

SECTION 3

Visual Explanation — How Callbacks Flow

The diagram below illustrates the flow of a callback function. On the left, the main program calls a higher-order function and passes a callback as an argument. The higher-order function does its work, and when it's ready, it invokes the callback. In the synchronous case this happens immediately; in the asynchronous case, the callback is placed on the event queue and executed once the call stack is clear.

Callback Function FlowMain ProgramdoTask(myCallback)Higher-Order Functionfunction doTask(cb)Callback Runscb(result)pass fninvokeSynchronous vs. AsynchronousSYNCHRONOUS1. Main calls doTask(cb)2. doTask runs its logic3. doTask calls cb(result)4. cb finishes5. doTask finishes6. Main continuesblockingASYNCHRONOUS1. Main calls setTimeout(cb, 1000)2. Timer starts in background3. Main continues immediately! ✓4. Timer finishes → cb enters queue5. Event loop picks up cb6. cb executesnon-blocking
In the synchronous path (left), the callback runs right away, and the main program waits. In the asynchronous path (right), the main program continues running while the task completes in the background, and the callback executes later via the event loop.

Notice the crucial difference: in the synchronous path, step 3 through step 5 all happen before the main program can move on. In the asynchronous path, the main program reaches step 3 immediately, freeing it to handle user interactions or other work while the timer (or network request, or file read) finishes in the background.

SECTION 4

How Callbacks Work Under the Hood

To truly understand callbacks, you need to know how JavaScript executes code. JavaScript has a single call stack — think of it as a stack of trays in a cafeteria. The program pushes a function onto the stack when it starts running, and pops it off when it finishes. Only the top tray (function) is being actively worked on at any moment.

When you call an asynchronous function like setTimeout(myCallback, 2000), JavaScript doesn't put myCallback on the call stack right away. Instead, the browser's timer API handles the 2-second wait separately. Once the timer expires, myCallback is placed into the callback queue (also called the task queue). The event loop constantly checks: "Is the call stack empty? If yes, grab the next callback from the queue and push it onto the stack."

Synchronous Callback Pattern

A synchronous callback is invoked immediately within the body of the higher-order function. Array methods like .forEach(), .map(), and .filter() are perfect examples. When you write [1, 2, 3].map(x => x * 2), the arrow function x => x * 2 is a callback that runs immediately for each element. Nothing is deferred.

Asynchronous Callback Pattern

An asynchronous callback is scheduled to run later. Common triggers include timers (setTimeout, setInterval), DOM events (addEventListener), and network requests (fetch or XMLHttpRequest). The key insight is that the code after the async call runs before the callback does. This often surprises beginners who expect code to execute top-to-bottom.

⚠

Common Gotcha

When you write setTimeout(greet, 0), the callback doesn't run at 0 milliseconds. It runs after the current call stack is empty, even though the delay is zero. The event loop must finish all synchronous code first.
JavaScript Event Loop & Callback QueueCALL STACKmain()setTimeout(cb, 2000)(empty after pop)↑ functions pushed/poppedWEB APIsTimer (2000ms countdown)DOM Events, fetch, etc.Handled by the browserdelegatesCALLBACK QUEUEcb (waiting)timer doneEVENT LOOPchecksstack empty?If yes → move cbto call stackExecution Order Exampleconsole.log('A');setTimeout(() => console.log('B'), 0);console.log('C');Output:ACB
This diagram shows JavaScript's event loop model. The call stack processes synchronous code. Asynchronous tasks are handed to Web APIs. When they complete, callbacks enter the callback queue. The event loop moves them to the call stack once it's empty — which is why 'B' prints after 'C'.

SECTION 5

Common Callback Patterns

Callbacks appear in many forms throughout JavaScript. Understanding the most common patterns will help you recognize them instantly in any codebase. Let's look at the four patterns you'll encounter most often.

Pattern 1: Event Handler Callbacks

When you attach a function to a DOM event — like a click, keypress, or form submission — that function is a callback. The browser's addEventListener method is a higher-order function that stores your callback and invokes it every time the specified event fires. For example, button.addEventListener('click', handleClick) passes handleClick as a callback that runs each time the button is clicked.

Pattern 2: Timer Callbacks

setTimeout(callback, delay) runs a callback once after a specified delay in milliseconds. setInterval(callback, interval) runs a callback repeatedly at set intervals. Both are asynchronous — the rest of your code continues executing while the timer counts down.

Pattern 3: Array Iteration Callbacks

Methods like .forEach(), .map(), .filter(), and .reduce() all accept callback functions. These are synchronous callbacks — they execute immediately for each element in the array. The callback typically receives the current element, its index, and the whole array as arguments.

Pattern 4: Error-First Callbacks (Node.js)

In Node.js, the convention is to pass an error as the first argument to every callback. If the operation succeeded, the error is null and the result appears as the second argument. If something went wrong, the error object contains details about the failure. This standardized pattern looks like: fs.readFile('data.txt', (err, data) => { ... }).

PatternSync or AsyncExampleWhen the Callback Runs
Event HandlerAsynchronousaddEventListener('click', cb)Each time the event fires
TimerAsynchronoussetTimeout(cb, 1000)After the delay elapses
Array IterationSynchronous[1,2,3].map(cb)Immediately, for each element
Error-First (Node)Asynchronousfs.readFile(path, cb)When the file read completes

Four common callback patterns in JavaScript

SECTION 6

Worked Example — Building a Callback Step by Step

Let's walk through creating and using a callback function from scratch. We'll build a function that simulates fetching a student's grade from a server and then processes the result using a callback.

Simulating an Async Data Fetch with a Callback

  1. 1

    Step 1 — Define the callback function

    First, we write the function that will process the data once it arrives. This is our callback. It takes a grade as an argument and logs a message. function displayGrade(grade) { console.log('Your grade is: ' + grade); }
    We now have a function displayGrade ready to be passed as a callback.
  2. 2

    Step 2 — Define the higher-order function

    Next, we create a function that simulates fetching data. It accepts a student name and a callback parameter. We use setTimeout to simulate a 2-second network delay. function fetchGrade(studentName, callback) { console.log('Fetching grade for ' + studentName + '...'); setTimeout(function() { const grade = 'A-'; // simulated result callback(grade); // invoke the callback with the result }, 2000); }
    The fetchGrade function is a higher-order function because it accepts a function (callback) as a parameter.
  3. 3

    Step 3 — Call the higher-order function and pass the callback

    Now we call fetchGrade and pass displayGrade as the callback. Notice we pass displayGrade without parentheses — we're passing the function itself, not calling it. fetchGrade('Alice', displayGrade); console.log('This runs while waiting!');
    Important: Writing displayGrade (no parentheses) passes the function reference. Writing displayGrade() would call the function immediately and pass its return value instead.
  4. 4

    Step 4 — Trace the output order

    Let's trace what the console prints and in what order: // Output: // Fetching grade for Alice... ← prints first (synchronous) // This runs while waiting! ← prints second (synchronous) // Your grade is: A- ← prints after 2 seconds (async callback)
    The line "This runs while waiting!" prints before the grade because setTimeout is asynchronous — the callback waits 2 seconds while the rest of the code continues.
💡

Pro Tip: Anonymous Callbacks

Instead of defining a separate named function, you can pass an anonymous function (or arrow function) directly: fetchGrade('Alice', (grade) => { console.log('Grade: ' + grade); });. This is more concise but can be harder to debug since the function has no name in stack traces.

SECTION 7

Strengths and Limitations of Callbacks

Callbacks are the original building block for asynchronous JavaScript, and they're powerful — but they also have some well-known drawbacks. Understanding both sides will help you choose the right tool for the right situation.

StrengthsLimitations
Simple and intuitive for single async operations — easy to understand what will happen when a task finishes.Callback Hell: Nesting multiple callbacks creates deeply indented, hard-to-read code (the dreaded "pyramid of doom").
Universal support — callbacks work in every version of JavaScript and every browser.Inversion of Control: You trust the higher-order function to call your callback at the right time, the right number of times, and with the right arguments.
Lightweight — no special objects or syntax needed. Just pass a function.Error handling is manual: You must check for errors in every callback individually. There's no built-in way to catch errors across a chain of callbacks.
Foundation for all async patterns — Promises, async/await, and Observables are all built on top of callbacks.Hard to compose: Running async operations in sequence or in parallel using only callbacks requires careful manual coordination.

Comparing the strengths and limitations of callback functions

✦

KEY TAKEAWAY

Think of callbacks like handing someone a list of instructions on a sticky note. For one simple task, a single sticky note works great. But imagine a chain of tasks where each sticky note says, "When you're done, read the next sticky note inside this one." After three or four levels of nesting, you've got a confusing pile of notes inside notes. That's callback hell — and it's why JavaScript introduced Promises as a cleaner way to chain tasks.

SECTION 8

Connection to Promises and Async/Await

Callbacks are the foundation, but modern JavaScript offers more structured ways to handle asynchronous code. Promises (introduced in ES6/2015) wrap an async operation in an object that can be in one of three states: pending, fulfilled, or rejected. Instead of nesting callbacks, you chain .then() calls. Async/await (introduced in ES8/2017) is syntactic sugar on top of Promises that lets you write asynchronous code that looks synchronous.

FeatureCallbacksPromisesAsync/Await
SyntaxdoTask(cb)doTask().then(cb)const r = await doTask()
Error HandlingManual (error-first pattern).catch() methodtry/catch block
ChainingNested (callback hell)Flat chain of .then()Sequential, line by line
ReadabilityDecreases with nestingBetter, but still chain-basedMost readable — looks synchronous
Browser SupportAll versionsModern browsers (ES6+)Modern browsers (ES8+)

Evolution of asynchronous patterns in JavaScript

Even though Promises and async/await are more modern, they don't replace callbacks — they're built on top of them. When you write .then(result => console.log(result)), the arrow function inside .then() is still a callback! Mastering callbacks gives you the mental model you need to understand every async pattern that came after.

ℹ

Looking Ahead

In future lessons, you'll learn how to convert callback-based code into Promises and then use async/await for clean, readable async code. But the event loop and callback queue concepts you've learned here remain the engine under the hood.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

In your own words, explain what a callback function is and why it is useful in JavaScript. What would happen if JavaScript had no mechanism for callbacks and could only run code sequentially?
Show answer
A callback function is a function passed as an argument to another function, which is then executed ("called back") at a later time. Without callbacks, JavaScript's single-threaded environment would freeze every time it encountered a slow operation like a network request or timer. The user interface would become completely unresponsive — no scrolling, clicking, or typing — because the thread would be stuck waiting. Callbacks allow the program to register what should happen after a task completes and then continue running other code in the meantime.

Question 2: BASIC CALCULATION

What is the exact output of the following code? Write the lines in the correct order. console.log('Start'); setTimeout(() => console.log('Timer'), 0); console.log('End');
Show answer
The output is: Start End Timer Even though the setTimeout delay is 0 milliseconds, the callback is still placed in the callback queue. The event loop only moves it to the call stack after all synchronous code has finished executing. Therefore, 'Start' prints first, 'End' prints second (both synchronous), and 'Timer' prints last once the event loop picks up the callback from the queue.

Question 3: INTERMEDIATE

Write a function called greetUser that takes a name (string) and a callback function. The function should create a greeting string like 'Hello, Alice!' and then pass that string to the callback. Then call greetUser with the name 'Alice' and a callback that logs the greeting to the console using console.log.
Show answer
function greetUser(name, callback) { const greeting = 'Hello, ' + name + '!'; callback(greeting); } greetUser('Alice', function(message) { console.log(message); }); // Output: Hello, Alice! The function greetUser is a higher-order function because it accepts callback as a parameter. It builds the greeting, then invokes the callback with that greeting as an argument. The anonymous function we pass in receives the greeting as message and logs it. This is a synchronous callback because it runs immediately inside greetUser.

Question 4: APPLIED

You're building a quiz app. Write a function called checkAnswer that takes three parameters: userAnswer (string), correctAnswer (string), and callback (function). If the answers match, the callback should be called with null as the first argument and 'Correct!' as the second. If they don't match, call the callback with an Error object as the first argument. Use the error-first callback pattern.
Show answer
function checkAnswer(userAnswer, correctAnswer, callback) { if (userAnswer === correctAnswer) { callback(null, 'Correct!'); } else { callback(new Error('Wrong answer. Expected: ' + correctAnswer)); } } // Usage: checkAnswer('Paris', 'Paris', (err, result) => { if (err) { console.log('Error: ' + err.message); } else { console.log(result); // 'Correct!' } }); This follows the Node.js error-first convention. The first argument to the callback is always the error (or null if no error). The second argument is the success data. The caller checks err first — if it's truthy, handle the error; otherwise, use the result. This pattern makes error handling predictable across all callbacks in a project.

Question 5: CRITICAL THINKING

Consider the following code that attempts to load three resources in sequence. Each load takes a callback. Explain why this pattern is problematic, and describe — in general terms — how you might refactor it to be more readable. (You do not need to write the refactored code.) loadResource('header.html', function(err, header) { if (err) { console.error(err); return; } loadResource('body.html', function(err, body) { if (err) { console.error(err); return; } loadResource('footer.html', function(err, footer) { if (err) { console.error(err); return; } renderPage(header, body, footer); }); }); });
Show answer
This code demonstrates callback hell (also called the "pyramid of doom"). Each nested callback shifts the code further to the right, making it difficult to read, debug, and maintain. Adding a fourth or fifth resource would make the indentation even deeper. Error handling is repetitive — the same if (err) check appears at every level. To refactor, you could: (1) extract each nested callback into a named function to flatten the structure, (2) use Promises so each step returns a .then() that chains linearly, or (3) use async/await to write the three loads as sequential lines with a single try/catch for error handling. The key insight is that callbacks themselves aren't the problem — it's deeply nesting them that creates unmanageable code.

SUMMARY

Lesson Summary

Summary

A callback function is a function passed as an argument to another function — known as a higher-order function — and is executed at a later time. This pattern is possible because JavaScript treats functions as first-class citizens. Synchronous callbacks (like those in .forEach() and .map()) run immediately, while asynchronous callbacks (like those in setTimeout and event listeners) are deferred and processed through the event loop and callback queue.

The four most common callback patterns are event handlers, timer callbacks, array iteration callbacks, and the error-first convention used in Node.js. While callbacks are simple and universally supported, deeply nested callbacks create callback hell. Modern alternatives like Promises and async/await build on top of callbacks to provide cleaner syntax, but understanding callbacks remains essential because they are the underlying mechanism of all asynchronous JavaScript.

Varsity Tutors • JavaScript • Callback Functions — Understand callback functions and common callback patterns (intro)