Callbacks in JavaScript_ a Comprehensive Guide
Callbacks in JavaScript_ a Comprehensive Guide
function sayGoodbye() {
console.log("Goodbye!");
}
In this code, greet takes two parameters: name and callback . After logging a greeting, greet
calls callback() . We pass the function sayGoodbye as the callback, so after printing “Hello, Alice!”,
the program prints “Goodbye!”.
Notice how the callback function is not executed immediately when passed, but is stored and later
invoked inside greet . We could also use an anonymous arrow function as the callback:
greet("Bob", () => {
console.log("This is an anonymous callback!");
});
// Output:
// Hello, Bob!
// This is an anonymous callback!
1
Here, an arrow function is passed directly as the callback argument and executed inside greet .
• Event Listeners: Web APIs like addEventListener use callbacks to respond to user
interactions. For example:
In this snippet, we register a click handler: when the button is clicked, the callback function is invoked
3 . The addEventListener method “sets up a function that will be called whenever the specified event
• Timers ( setTimeout / setInterval ): Timing functions take callbacks to run code after a
delay. For example:
console.log("Start");
setTimeout(() => {
console.log("Async callback after 1 second");
}, 1000);
console.log("End");
// Output:
// Start
// End
// (after ~1 second) Async callback after 1 second
Here, setTimeout schedules the arrow-function callback to run after 1000 ms. setTimeout “sets a
timer which executes a function … once the timer expires” 4 . The output shows that “End” is logged
before the callback runs, demonstrating asynchronous timing.
• Array Iteration: Many array methods take callbacks for processing elements. For example,
forEach calls a callback on each array element 5 :
2
The forEach method “executes a provided function once for each element in an array” 5 . Other
methods like map , filter , and reduce similarly use callbacks to transform or filter arrays.
• Higher-Order Functions: Functions that take callbacks (or return functions) are called higher-
order functions. Any time you pass a function as an argument (like the examples above), you’re
using a higher-order function 6 . This ability makes JavaScript flexible and modular.
console.log("First");
setTimeout(() => {
console.log("Third (inside setTimeout)");
}, 500);
console.log("Second");
First
Second
Third (inside setTimeout)
Because setTimeout is asynchronous, the code does not wait 500ms before logging “Second”.
Instead, “Second” appears immediately, and the callback runs later (after about 500ms). This behavior is
central to non-blocking programming: the event loop handles the timer’s expiration and invokes the
callback when ready.
3
Figure: Node.js event loop with multiple callback queues (timers, I/O, check, close) and microtask queues
(nextTick, Promises). The event loop coordinates when callbacks run.
The event loop (illustrated above) continuously checks the call stack and task queues. When the stack is
empty, it takes the next callback from a queue and pushes it to the stack for execution 8 9 . This
mechanism allows JavaScript to handle multiple async tasks (timers, network requests, etc.) without
freezing the UI 7 10 .
doTaskA(arg, function(resultA) {
doTaskB(resultA, function(resultB) {
doTaskC(resultB, function(resultC) {
console.log("Final result:", resultC);
// ... and so on ...
});
});
});
Each step depends on the previous one, so we nest callbacks. As more steps are added, the code forms
a growing pyramid. According to authors, when “we nest multiple callbacks within a function, the shape
of the resulting code structure resembles a pyramid… it makes the code very difficult to understand and
maintain” 11 . This anti-pattern complicates both control flow and error handling. It was a major pain
point in early JavaScript development 12 11 .
4
In short, callback hell is “an ineffective way of writing code asynchronously” 13 , and developers often try
to avoid it through other patterns.
By chaining .then() handlers, you avoid deep nesting. For example, converting a nested call to
promises:
doTaskA(arg)
.then(resultA => doTaskB(resultA))
.then(resultB => doTaskC(resultB))
.then(resultC => {
console.log("Final result:", resultC);
})
.catch(err => {
console.error("Error occurred:", err);
});
Here each step returns a promise, and the next .then() runs after the previous promise resolves.
There is no pyramid of indentation. As one tutorial notes, chaining .then() calls “allows the
consumption of promises to appear more synchronous than callbacks, as they do not need to be
nested” 15 . The result is more linear and readable code.
This promise-based delay replaces a callback-style setTimeout . Promises also have an explicit
mechanism for errors: .catch() handles rejections, whereas callbacks require manual error checks.
5
Figure: Flowchart of Promise states and chaining. A Promise starts pending, then becomes fulfilled (with
.then ) or rejected (with .catch ). Chaining .then() returns new promises for sequential async steps.
As illustrated above, a pending promise moves to fulfilled or rejected, and the appropriate handler runs
16 . The MDN guide notes that promises allow “asynchronous methods to return values like synchronous
methods” by returning a placeholder that will get the value in the future 14 . This design simplifies many
patterns: ES2015 APIs like fetch() use promises, and developers can sequence operations cleanly.
fetch(url)
.then(response => response.json())
.then(data => {
console.log("Data received:", data);
})
.catch(error => {
console.error("Error:", error);
});
6
}
fetchData();
This async function pauses at each await until the promise resolves or rejects 17 . If an error is
thrown (or a promise rejects), it can be caught in the surrounding try/catch . In effect, async/await is
“enabling asynchronous, promise-based behavior to be written in a cleaner style and avoiding the need to
explicitly configure promise chains” 17 . Under the hood, async / await still use promises, but they
greatly improve readability.
Advanced Concepts
Higher-Order Functions
A higher-order function is one that takes another function as an argument or returns a function 6 .
Callbacks enable higher-order patterns. For example:
function repeat(action) {
action();
action();
}
function sayHi() {
console.log("Hi!");
}
repeat(sayHi);
// Output:
// Hi!
// Hi!
Here, repeat is a higher-order function because it accepts action (a function) as a parameter and
calls it. Higher-order functions make code more reusable and abstract. Many built-in functions
( forEach , map , etc.) and libraries rely on this idea. By passing different callbacks, you can customize
behavior.
Function Composition
Function composition involves combining simple functions to build more complex ones. You can write
utilities that return a new function which calls callbacks in sequence. For example:
function compose(f, g) {
return function(x) {
return f(g(x));
};
}
const greet = name => `Hello, ${name}`;
const exclaim = sentence => sentence + "!";
const excitedGreet = compose(exclaim, greet);
console.log(excitedGreet("Alice")); // Hello, Alice!
7
In compose , we return a new function that, when called, runs g on the input and then runs f on
the result. Here, compose(exclaim, greet) returns a function that first greets the name, then adds
an exclamation mark. This is a simple example of function composition using callbacks. Such patterns
are common in functional programming.
const fs = require('fs');
fs.readFile('file.txt', (err, data) => {
if (err) {
console.error("Read error:", err);
return;
}
console.log("File data:", data.toString());
});
In this Node.js example, fs.readFile invokes the callback with an err if something went wrong,
or with the data if successful. GeeksforGeeks explains: “The first argument is reserved for the error
object by the function. This error object is returned by the first argument whenever any error occurs” 19 . We
check if (err) , handle it, and then proceed.
This pattern forces you to explicitly manage errors in each callback. If you forget to check err , bugs
can sneak in. Unlike exceptions in synchronous code, you can’t use try/catch around an
asynchronous callback to catch errors thrown inside it (the callback runs later, outside the original call
stack). Instead, you must either check the error argument or use the promise equivalent (which can use
.catch() or try/catch with await ).
In more complex callback chains, propagating errors becomes cumbersome, which is another reason
promises and async/await were introduced. With promises, a thrown error or rejected promise in any
step skips to the nearest .catch() . With async/await, you can wrap multiple awaits in a single try/
catch . Nonetheless, understanding the callback pattern is important, since many legacy APIs and
Node.js functions still use it.
Summary
A callback in JavaScript is simply a function passed into another function to be executed later. They are
the backbone of JavaScript’s event-driven and asynchronous nature 1 2 . We covered how to write
callbacks with examples: event handlers ( addEventListener ), timers ( setTimeout ), and array
methods ( forEach ) all use callbacks 3 4 5 . In asynchronous code, callbacks let JavaScript
continue processing while waiting for tasks to complete. However, complex nested callbacks lead to
“callback hell” (the pyramid of doom) 11 12 . To address this, JavaScript evolved: Promises offer
chained handlers, improving readability and error handling 15 14 . Async/await builds on promises to
make async code look synchronous 17 18 . We also saw advanced topics: higher-order functions,
function composition, and the Node.js error-first callback convention 6 19 .
8
By moving from callbacks to promises and async/await, JavaScript developers can write clearer and
more maintainable asynchronous code. Yet callbacks remain a key concept: they enable event-driven
programming and functional patterns that are idiomatic in JavaScript. Understanding callbacks
thoroughly—from basic syntax to advanced usage—empowers you to work with any JavaScript API,
legacy or modern, and to write robust async code.
1 Callback function - MDN Web Docs Glossary: Definitions of Web-related terms | MDN
https://developer.mozilla.org/en-US/docs/Glossary/Callback_function
2 JavaScript Callbacks
https://www.w3schools.com/js/js_callback.asp