π Callback Functions
Callbacks are ordinary functions with a particular job. You give a function to some other code, and that code decides when to call it.
Here, sayHello is the callback. The timer receives it now and calls it later.
Most callback confusion comes from one small pair of parentheses:
That distinction is the heart of this chapter.
Useful background
This chapter assumes you already know that JavaScript functions can be passed around as values. If that idea is new, begin with First-Class Functions and Higher-Order Functions.
βοΈ What Makes a Function a Callback?
JavaScript has no callback keyword. A function becomes a callback because of how
it is used.
function runTask(task) {
task();
}
runTask(function printMessage() {
console.log('Task is running');
});
printMessage is passed into runTask. The parameter named task holds that
function, and task() invokes it.
The code receiving a callback controls the callback's contract:
- when it runs;
- how many times it can run;
- which arguments it receives;
- what happens to its return value;
- how errors are handled.
Callback does not mean asynchronous
Array.prototype.map() invokes its callback immediately for every item.
setTimeout() invokes its callback later. Both functions accept callbacks,
but their timing is different.
πΆοΈ Anonymous Function and Callback Are Not Synonyms
Anonymous describes whether a function has a name. Callback describes how the function is used. This is an anonymous callback:
A callback can also be named:
See First-Class Functions for the different ways functions can be declared and named.
βοΈ Passing a Function vs. Calling It
Suppose we want to display a reminder after one second:
Calling it too early
JavaScript evaluates function arguments before it calls setTimeout. The order is:
- Call
showReminder()immediately. - Take its return value, which is
undefinedhere. - Pass that return value to
setTimeout.
The timer never receives the function.
Passing the function
Without parentheses, showReminder refers to the function itself. The timer can
store that reference and call it after the delay.
Passing a wrapper
The anonymous function is created immediately, but its body does not run yet.
showReminder() is inside that body, so it waits until the timer calls the
wrapper.
The arrow-function version works the same way:
Read the code literally
setTimeout(showReminder(), 1000)means βcall it now and pass its result.βsetTimeout(showReminder, 1000)means βpass it so the timer can call it.βsetTimeout(() => showReminder(), 1000)means βpass a function that will call it.β
π¦ When a Wrapper Is Useful
Pass the original function when it already has the shape you need:
Use a wrapper when you need to supply arguments:
Calling greet directly would run it too early:
A wrapper can also make a decision at execution time:
The visibility check happens when the callback runs, not when the timer is created.
π§© The Caller Supplies the Arguments
A callback does not choose which arguments it receives. The code invoking it does.
For example, map() passes the current value, its index, and the original array:
const colours = ['Blue', 'Green'];
const labels = colours.map(function (colour, index) {
return `${index + 1}. ${colour}`;
});
console.log(labels); // ["1. Blue", "2. Green"]
This is why a wrapper can be useful when an existing function expects a different signature:
map passes the index as the second argument. parseInt treats that argument as
the radix. A wrapper adapts one contract to the other:
β©οΈ Return Values and Async Boundaries
return sends a value back to the function's caller. It does not travel backwards
through time to a function that has already finished.
function getMessage() {
setTimeout(() => {
return 'Finished';
}, 1000);
}
console.log(getMessage()); // undefined
getMessage() finishes before the timer callback runs. When the callback later
returns 'Finished', it returns that value to the timer machinery, which does not
use it.
In callback-style code, pass the result to another callback:
function getMessage(onComplete) {
setTimeout(() => {
onComplete('Finished');
}, 1000);
}
getMessage(function (message) {
console.log(message);
});
Promises provide a cleaner way to represent a future result. That subject belongs
in Promises, where chaining, rejection, and async/await are
covered properly.
πͺͺ Function Identity Matters
Every function expression creates a new function object:
This matters when an API expects the same callback reference later. The following code does not remove the listener:
button.addEventListener('click', function () {
console.log('Clicked');
});
button.removeEventListener('click', function () {
console.log('Clicked');
});
The two functions look identical, but they are different objects. Store or name the callback when you will need it again:
function handleClick() {
console.log('Clicked');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);
The same issue appears when setup code creates a fresh listener every time it runs:
Each call to render() creates another function. Long-lived listeners should
normally be registered once or removed during cleanup.
π A Quick Debugging Routine
When a callback behaves unexpectedly, ask:
- Am I passing the function or calling it?
- Who invokes the callback?
- Does it run now or later?
- Which arguments does the caller supply?
- Can it run more than once?
- Will I need the same function reference for cleanup?
Once the hand-off itself is clear, follow the topic that owns the remaining problem:
| Problem | Continue with |
|---|---|
| A callback sees an unexpected outer variable | Closures |
A method loses its this value |
call, apply, and bind |
| A timer or event runs in an unexpected order | Asynchronous JavaScript and the Event Loop |
| Nested callbacks hide the workflow | Callback Hell |
| A future result or error is difficult to pass along | Promises |
π§ Keep This Picture in Your Head
function save() {} // Create a function.
const task = save; // Store or pass the function.
task(); // Invoke the function.
const later = () => { // Create a wrapper.
save(); // This runs when later() is invoked.
};
setTimeout(later, 1000);
If you can identify who owns each pair of parentheses and when that line runs, you can follow most callback code without guessing.
π Continue Learning
- First-Class Functions for function forms and values
- Higher-Order Functions for functions that receive or return functions
- Closures for variables remembered by callbacks
setTimeoutand Closures for timer loop questions- Asynchronous JavaScript and the Event Loop for scheduling
- Callback Hell for nested asynchronous workflows
- Promises for future values and error propagation