What Is a Callback Function in JS: Full Tutorial

A callback is a function passed into another function to be called later. Learn synchronous and asynchronous callbacks, error-first patterns, and how callbacks power event handlers and async code.

6 min read

A callback is a function passed as an argument to another function, to be called later. The receiving function decides when to call it -- immediately, after a timer, when an event fires, or when data arrives.

javascriptjavascript
function greet(name) {
  console.log(`Hello, ${name}!`);
}
 
function processUser(callback) {
  const userName = "Alex";
  callback(userName); // Calls the callback with the user name
}
 
processUser(greet); // "Hello, Alex!"

The greet function is the callback. processUser is the function that receives it. greet does not run until processUser says so, and processUser controls the exact moment and the exact argument it gets called with. This separation, one function provides the timing, the other provides the behavior, is the callback pattern.

Synchronous vs Asynchronous Callbacks

Callbacks come in two forms:

Sync vs async callbacks

A synchronous callback runs right away, before the outer function returns. An asynchronous callback is scheduled to run later, after the outer function has already returned.

SynchronousAsynchronous
RunsImmediately, before the outer function returnsLater, after the outer function has returned
Examplesmap, filter, forEachsetTimeout, fetch, event listeners
Blocks the next line?Yes, until the callback finishesNo, execution continues immediately

Synchronous Callbacks

Array methods are the most common synchronous callbacks. The callback runs for each element before the method returns:

javascriptjavascript
const numbers = [1, 2, 3];
 
// The callback (n => n * 2) runs three times, synchronously
const doubled = numbers.map(n => {
  console.log(`Processing ${n}`);
  return n * 2;
});
 
console.log(doubled);
// Processing 1
// Processing 2
// Processing 3
// [2, 4, 6]

The map method does not return until every callback has finished. This is synchronous: the code runs in order, top to bottom.

Other synchronous callback examples include forEach, filter, reduce, find, some, every, and sort.

Asynchronous Callbacks

An asynchronous callback is registered now but executed later. The outer function returns immediately, and the callback runs when the event occurs:

javascriptjavascript
console.log("Before setTimeout");
 
setTimeout(() => {
  console.log("Inside the callback");
}, 1000);
 
console.log("After setTimeout");
 
// Output order:
// Before setTimeout
// After setTimeout
// Inside the callback  (after 1 second)

setTimeout registers the callback and returns immediately. The callback runs later, after the timer expires. The program does not pause and wait.

Event Handlers Are Callbacks

Every DOM event listener is a callback:

javascriptjavascript
const button = document.querySelector("button");
 
button.addEventListener("click", function(event) {
  console.log("Button clicked!", event.target);
});

The function passed to addEventListener is a callback. It does not run when the line executes. It runs later, when the user clicks the button.

Error-First Callbacks

Node.js popularized the error-first callback convention. The first argument is always an error object, or null when nothing went wrong, and the second is the result:

javascriptjavascript
function readConfig(path, callback) {
  // Simulating reading a file
  setTimeout(() => {
    if (path === "") {
      callback(new Error("Path is required"), null);
    } else {
      callback(null, { theme: "dark", language: "en" });
    }
  }, 500);
}

Calling readConfig follows the same error-first shape on the receiving end: check the first argument before trusting the second one. This mirrors the convention on both sides of the call, which is exactly why it became a de facto standard across Node.js libraries:

javascriptjavascript
readConfig("config.json", (err, config) => {
  if (err) {
    console.error("Failed:", err.message);
    return;
  }
  console.log("Config loaded:", config);
});

The pattern is always: check if the error argument exists first. If it does, handle the error and return. If not, use the result. While promises and async/await have largely replaced this pattern for new code, you will still encounter it in Node.js libraries and legacy code.

Callbacks in Custom Functions

You can write your own functions that accept callbacks:

javascriptjavascript
function fetchData(url, onSuccess, onError) {
  // Simulating a network request
  setTimeout(() => {
    if (url.startsWith("https://")) {
      onSuccess({ data: "Response from " + url });
    } else {
      onError(new Error("Only HTTPS URLs are allowed"));
    }
  }, 1000);
}

This variant uses two separate callbacks instead of the error-first style, one for success and one for failure, which reads clearly at the call site:

javascriptjavascript
fetchData(
  "https://api.example.com/users",
  (data) => console.log("Success:", data),
  (err) => console.error("Error:", err.message)
);

This separates the fetching logic from what happens with the result. The same fetchData function could be used to load users, products, or settings, only the callbacks change.

Callback Hell and How to Avoid It

When callbacks are nested inside callbacks, the code drifts right and becomes hard to read:

javascriptjavascript
// Callback hell
getUser(id, (err, user) => {
  if (err) return handleError(err);
  getPosts(user.id, (err, posts) => {
    if (err) return handleError(err);
    getComments(posts[0].id, (err, comments) => {
      if (err) return handleError(err);
      console.log(comments);
    });
  });
});

Solutions:

  • Name your callbacks instead of using deeply nested anonymous functions.
  • Use Promises and async/await for sequential async operations.
  • Modularize -- split each level into its own named function.

For the modern approach to async, see the async JavaScript articles. For the broader pattern, see Higher Order Functions in JavaScript: Full Guide. For how to pass functions as arguments, see How to Pass a Function as an Argument in JS Guide.

Rune AI

Rune AI

Key Insights

  • A callback is a function passed as an argument to another function to be invoked later.
  • Synchronous callbacks execute immediately (array methods, custom iteration).
  • Asynchronous callbacks execute after an operation completes (timers, events, fetch).
  • The error-first pattern (err, result) is a convention for Node.js-style callbacks.
  • Callbacks power addEventListener, setTimeout, array methods, and all promise .then() handlers.
RunePowered by Rune AI

Frequently Asked Questions

What is callback hell?

Callback hell is deeply nested callbacks that make code hard to read and maintain. It happens when you chain multiple asynchronous operations. The solution is to use named functions, Promises, or async/await instead of deeply nested anonymous callbacks.

Are callbacks still relevant with Promises and async/await?

Yes. Many built-in APIs still use callbacks (setTimeout, addEventListener, array methods). Promises and async/await are built on top of the callback concept. Understanding callbacks is essential for understanding how async JavaScript works.

What is the difference between a callback and a higher-order function?

A higher-order function is the function that receives or returns another function. A callback is the function that gets passed in. In arr.map(callback), map is the higher-order function and the function you pass is the callback.

Conclusion

A callback is a function you give to another function to call later. It is the simplest form of 'do this when that happens.' Synchronous callbacks run immediately inside array methods and custom iterators. Asynchronous callbacks wait for timers, events, or network responses. Understanding callbacks is the foundation for understanding promises, async/await, and the entire asynchronous side of JavaScript.