Skip to main content

Callback function

A callback function is a function passed as an argument to another function so that it runs later, after some event happens or an operation finishes. Put simply: "I am handing you a function, call it when you are ready."

Theory

TL;DR

  • A callback is a function passed as an argument that the receiving function calls at the right moment.
  • This works because functions in JavaScript are first-class objects and can be passed as values.
  • A callback can be named or anonymous, an arrow function in particular.
  • A callback is not necessarily asynchronous: array.map(fn) calls it synchronously, setTimeout(fn) does not.
  • setTimeout, addEventListener and fetch().then() are all built on callbacks.
  • Deep nesting of callbacks is called callback hell, and Promise plus async/await cure it.

Quick example

javascript
function sayHello(name) { console.log(`Hello, ${name}!`); } function greet(callback) { console.log('Getting ready to say hello...'); callback('Oleh'); } greet(sayHello); // Output: // Getting ready to say hello... // Hello, Oleh!

What happens:

  1. sayHello is the callback passed into greet;
  2. greet calls it from inside itself;
  3. two lines are printed to the console, in the order the code runs.

Note that sayHello is passed to greet without parentheses. greet(sayHello()) would call the function immediately and pass its result, that is, undefined.

How it works

  • JavaScript accepts a callback as a value, since functions here are first-class objects.
  • The receiving function does not call it immediately, it stores it for a later call.
  • When the right moment arrives (the timer fired, the request finished, the event happened), the engine calls that function.

A callback does not have to be named, an anonymous function is often passed instead:

javascript
setTimeout(() => { console.log('Two seconds have passed'); }, 2000);

Here () => console.log(...) is the callback function, and it will be called in two seconds, not right away.

Callbacks in real situations

1. An event handler. Here the browser calls the callback when the user clicks:

javascript
button.addEventListener('click', () => { console.log('Button clicked!'); });

2. Loading data. The .then() callbacks are called after the asynchronous request completes:

javascript
fetch('/user') .then(response => response.json()) .then(data => console.log(data));

3. A step in an algorithm. A callback is not necessarily tied to asynchrony, it can simply be a way to plug your own logic in:

javascript
function calculate(a, b, callback) { const result = a + b; callback(result); } calculate(2, 3, (sum) => { console.log('Result:', sum); });

The callback hell problem

When there are too many callbacks, the code becomes nested and unreadable:

javascript
getUser(id, user => { getPosts(user, posts => { getComments(posts, comments => { console.log('Done!'); }); }); });

This is called callback hell: every next step shifts the code further to the right, error handling is duplicated at each level, and the sequence of actions becomes hard to follow.

How to get rid of the nesting

Promises appeared to avoid these infernal nestings:

javascript
getUser(id) .then(getPosts) .then(getComments) .then(console.log);

And then async/await, which gives asynchronous code the look of ordinary sequential code:

javascript
const user = await getUser(id); const posts = await getPosts(user); const comments = await getComments(posts); console.log('Done!');

A simple analogy for an interview:

Imagine you order a pizza and say: "Call me when it is ready." That call is the callback.

Summary:

TermDescription
Callback functionA function passed into another function to be run later
When it is calledAfter an event, after an operation completes, or on a condition
Usage examplessetTimeout, addEventListener, fetch().then()
The problemDeep nesting (callback hell)
Modern alternativesPromise, async/await

Common mistakes

  • Passing a call instead of a function. setTimeout(fn(), 1000) runs fn immediately and hands the timer its result. The correct form is setTimeout(fn, 1000).
  • Assuming every callback is asynchronous. array.forEach(fn) and array.map(fn) call the callback synchronously, right now, and defer nothing.
  • Losing this. A regular function used as a callback gets its own this. Use an arrow function or bind.
  • Ignoring errors. In the error-first style the first argument is the error: (err, data) => { if (err) ... }. Skip it and failures are lost silently.
  • Calling the callback twice. Without a return after an error the function keeps going and calls the callback again, which breaks all the logic above it.
  • Building deep pyramids. Three or more levels of nesting is a signal to rewrite the code with Promise or async/await.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.