Skip to main content

Callback function

A callback function is a function passed as an argument into another function, which calls it later, once it has finished its own work. Callbacks are normally used to run an action after an event, an operation or an asynchronous process, for example after data has loaded, a timer has fired or a click has happened.

Theory

TL;DR

  • A callback is a function argument that is called by the function you handed it to, not by your own code.
  • This works because functions in JavaScript are first class values: they can be passed around and returned.
  • Callbacks can be synchronous (map, filter, forEach) or asynchronous (setTimeout, event handlers, requests).
  • Pass a reference to the function, not its call: setTimeout(fn, 100), not setTimeout(fn(), 100).
  • Deeply nested callbacks turn into callback hell, which Promises and async/await are there to cure.

Quick example

javascript
function fetchData(callback) { setTimeout(() => { const data = 'Data from the server'; callback(data); // call the function we were given }, 1000); } fetchData(result => console.log(result)); // "Data from the server"

fetchData knows nothing about what will be done with the result: it receives that part of the behaviour from outside.

Why it works: functions as values

In JavaScript a function is an ordinary value, so it can be assigned to a variable, put in an array or passed to another function. A function that accepts or returns another function is called a higher-order function, and the function that is passed in is the callback.

javascript
const numbers = [1, 2, 3]; // map is the higher-order function, the arrow function inside is the callback const doubled = numbers.map(n => n * 2); // [2, 4, 6]

Synchronous and asynchronous callbacks

A synchronous callback runs immediately, within the same function call. An asynchronous one is queued and runs later, once the call stack is empty.

javascript
// synchronous: runs before the next line [1, 2, 3].forEach(n => console.log(n)); // asynchronous: runs after all synchronous code setTimeout(() => console.log('later'), 0); console.log('now'); // console order: 1, 2, 3, now, later

Error handling: the error-first style

Node.js established a convention: the first parameter of a callback is the error, and the data follows it. That way the caller can handle a failure in the same place where it receives the result.

javascript
function loadUser(id, callback) { setTimeout(() => { if (!id) { callback(new Error('id is required')); return; } callback(null, { id, name: 'Maria' }); }, 100); } loadUser(7, (err, user) => { if (err) { console.error(err.message); return; } console.log(user.name); // Maria });

Callback hell and the modern alternative

When one asynchronous operation depends on another, callbacks start nesting inside each other and the code drifts to the right like a staircase. That is callback hell: hard to read, and even harder to handle errors in.

javascript
loadUser(7, (err, user) => { loadOrders(user.id, (err, orders) => { loadItems(orders[0].id, (err, items) => { console.log(items); // nesting grows with every step }); }); });

The modern answer is Promises and async/await: the same sequence is written flat, and errors are caught by a single try/catch.

javascript
async function showItems() { try { const user = await loadUser(7); const orders = await loadOrders(user.id); const items = await loadItems(orders[0].id); console.log(items); } catch (err) { console.error(err.message); } }

Common mistakes

  • Passing the result of a call instead of the function itself: setTimeout(sayHi(), 100) runs sayHi right away and hands the timer undefined. The correct form is setTimeout(sayHi, 100).
  • Expecting a value received inside an asynchronous callback to be available right after the function call: it only appears once the callback actually runs.
  • Ignoring the first error argument in error-first callbacks and carrying on with empty data.
  • Calling the callback twice, both on success and on failure, because the return after the error branch was forgotten.
  • Losing this when an object method is passed as a callback: element.addEventListener('click', obj.handler) calls the method with a different this. An arrow function or bind fixes it.
  • Building deeply nested callbacks where Promises or async/await would do the job.

Short Answer

Interview ready
Premium

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