Understanding JavaScript Callbacks: A Beginner's Guide

himanshuhimanshu
8 min read

Introduction to Callback Functions

Functions in JavaScript

Before learning about Callback function, we first need to know what are functions in JavaScript. Functions are first-class objects in JavaScript. We can assign variables to them or passing them like arguments into other functions.

// declaring the function 
function functionName(parameters) {
    // something is written 
}

// calling functions 
functionName(arguments);

Callback functions

A Callback functions are the functions that are passed as an arguments to another functions, which is then executed after the completion of some operations. A function that accepts another function as an argument is called “higher-order function“, which contains the logic for when the callback function get executed.

Why are callbacks used?

In JavaScript, callbacks are used to handle asynchronous operations and allow code to run non-blockingly — meaning the program does not have to wait for one task to finish before moving on to the next.

Real Life Analogy

A real life analogy of callback functions is ordering food in restaurant.

Analogy

  1. You place your order (the initial request) with the waiter.

  2. The waiter takes your order to the kitchen (the asynchronous operation).

  3. While waiting, you can continue with other activities (not being blocked by the operation).

  4. When your food is ready (the operation is complete), the waiter calls you back (the callback function) to notify you that your food is ready for pickup or delivery.

This analogy illustrates how callback functions work in JavaScript, enabling asynchronous operations and ensuring that you're notified when a task is completed.

Basic Syntax and Examples

Syntax

function getsCallback(callback) {
    callback();
}

function doSomething() {
    // do something
}

getsCallback(doSomething);

Example :

function fetchData(callback) {
  setTimeout(() => {
    const data = { id: 1, name: "Alice" };
    callback(data);
  }, 2000); // Simulating a delay of 2 seconds
}

fetchData((data) => {
  console.log("Data received:", data);
});

Here, fetchData simulates fetching data after a 2-second delay and then calls the callback function with the received data.

Common Use Cases

Callback functions are fundamental concept of JavaScript, and they have numerous practical applications. Here are some of the common use cases of callback functions:

Handling Asynchronous Operations

  • Fetching Data from APIs : Callback functions are used to handle data received from APIs, allowing programs to continue working without waiting for the data to be fetched.

  • Reading Files : Callbacks are used to read files asynchronously, ensuring that the program remains responsive.

function fetchData(callback) {
  // Simulating a delay
  setTimeout(() => {
    const data = { id: 1, name: "Alice" };
    callback(data);
  }, 2000);
}
fetchData((data) => {
  console.log("Data received:", data);
});

Event Handling in DOM

  • User Interactions : Callback functions are used to respond to user interactions like click, input and other events.
document.getElementById("myButton").addEventListener("click", function() {
  console.log("Button clicked!");
});

Array Methods

Used in many array methods like map, reduce, sort.

const multiply2 = function(element) {
    return element * 2;
}

const arr = [1,2,3]

const num = arr.map(multipy2); // [2, 4, 6]

Custom Callbacks

In JavaScript, we can create functions that accepts other functions as an arguments. This is the powerful feature of JavaScript’s first-class functions.

A custom callback function is a function which we can pass as an argument to another function, which invokes it after some appropriate time — usually after the operation is complete.

function processUserInput(name, callback) {
  console.log("Processing user input...");
  callback(name);
}

function greet(name) {
  console.log("Hello, " + name + "!");
}

processUserInput("Viewer", greet);

The Problem with Callbacks

What is Callback hell?

Callback hell refers to a situation where multiple nested callbacks functions make the code difficult to read, debug and modify it later.

Lets take an example of a nested callbacks,

getData(function(a) {
  getMoreData(a, function(b) {
    getEvenMoreData(b, function(c) {
      getYetEvenMoreData(c, function(d) {
        getFinalData(d, function(finalData) {
          // Finally do something with all the data
          console.log(finalData);
        }, errorCallback);
      }, errorCallback);
    }, errorCallback);
  }, errorCallback);
}, errorCallback);

This callback hell is also know as “Pyramid of Doom“.

Problem caused

  • Readability : The code grow horizontally which look like pyramid making it harder to read.

  • Maintainability : Because the code is hard to read, it also lead to problem in maintaining the code.

  • Error handling : Error handling becomes repetitive and convoluted.

  • Sequential reasoning : Hard to understand the program’s flow and sequence of operations.

Callback vs Promise vs Async/Await

FeatureCallbacksPromisesAsync/Await
SyntaxNested functions passed as argumentsChain of .then() and .catch() methodsUses async function declaration and await keyword
IntroducedEarly JavaScriptES6 (2015)ES8 (2017)
Error HandlingCustom error parameters or try/catch blocks around entire operation.catch() methodStandard try/catch blocks
Code StructureCan lead to "callback hell" with multiple levels of nestingFlattened chain but still requires method chainingLooks like synchronous code, minimal nesting
ReadabilityPoor with multiple callbacksBetter than callbacksBest, resembles synchronous code
DebuggingDifficult (stack traces are less helpful)ImprovedBest (clear stack traces)
Sequential OperationsNested callbacksChain of .then() callsSimple sequence of await statements
Parallel OperationsCustom implementationBuilt-in with Promise.all(), Promise.race()Use Promise.all() with await
Underlying MechanismFunctionsPromise objectsPromises (syntactic sugar)
Control FlowManual handling requiredBuilt-in promise methodsLooks like standard control flow
CancelabilityCan be implementedNot built-in (requires workarounds)Not built-in (requires workarounds)
ExamplegetData(function(data) { ... })getData().then(data => ...)const data = await getData()

When to use Each

  • Callbacks: Legacy code or simple cases

  • Promises: When combining multiple async operations or when working with libraries that return promises

  • Async/Await: Most modern code, especially for sequential async operations or complex logic

Handling Errors in Callback functions

Error handling in callback functions is a key aspect of writing robust JavaScript code.

Error-first callbacks

The most common pattern in Node.js many JavaScript libraries is the “error-first“ callbacks:

function getData(data, callback) {
    if (!data) {
        callback(new Error("data is required"), null);
        return
    }

    data = "this is modified data"
    callback(null, data)
}

getData("currentdata", (err, msg) => {
    if (err) {
        console.error("error occurred", err)
    }
    else {
        console.log(msg)
    }
})

Writing our own Callback functions

Callback functions are fundamental to JavaScript programming, especially when dealing with asynchronous operations. Here’s how to write and implement your own callbacks functions effectively:

Basic Callback Function Pattern

function doSomething(callback) {
  // Do some work...
  console.log("Task is being performed");

  // Then execute the callback
  callback();
}

// Using the function with a callback
doSomething(function() {
  console.log("Callback executed after task completed");
});

Passing Data to Callbacks

function getData(callback) {
    const data = {name: "himanshu", id: 2}
    callback(data)
}

getData((data) => { console.log(`Name : ${data.name}, Id : ${data.id}`) })

This is how you can create your own callback functions.

Tips and Best Practices

Whether you are beginner or making your own product, using callbacks effectively can make your code cleaner, more readable, and easier to maintain. Here are some essential tips and best practices when working with callback functions in JavaScript:

Use Named Functions for Readability

Avoid using anonymous functions in callback function as it make the code difficult read and debug. Use named functions.

function onDataReceived(data) {
  console.log("Received:", data);
}

fetchData(onDataReceived);

Handle Errors Gracefully

If your callback might receive an error (especially in Node.js-style callbacks), always check and handle it.

function readFileCallback(err, data) {
  if (err) {
    console.error("Error reading file:", err);
    return;
  }
  console.log("File contents:", data);
}

Avoid Callback Hell (Pyramid of Doom)

Don’t let your code become a deeply nested mess of callbacks. Instead:

  • Use named functions

  • Flatten logic

  • Consider using Promises or Async/Await for better structure

Bad:

doA(() => {
  doB(() => {
    doC(() => {
      doD(() => {
        console.log("Done");
      });
    });
  });
});

Good:

function doA(callback) { /* ... */ }
function doB(callback) { /* ... */ }

doA(() => handleB());

function handleB() {
  doB(() => handleC());
}

Avoid Using Callback for Synchronous Code

Callbacks shine in asynchronous situations. Using them for purely synchronous code may add unnecessary complexity.

Avoid:

function dataToUpperCase(data, callback) {
    callback(data.toUpperCase());
}

dataToUpperCase("call", (msg) => { console.log(msg) })

Good:

function dataToUpperCase(data) {
    console.log(data.toUpperCase())
}

dataToUpperCase("call")

Don’t Forgot to Call Callback Function

A common mistake is forgetting to call callback inside your own custom functions.

Oops:

javascriptCopyEditfunction greet(name, callback) {
  console.log("Hi " + name);
  // forgot to call callback!
}

Correct:

javascriptCopyEditfunction greet(name, callback) {
  console.log("Hi " + name);
  if (typeof callback === "function") {
    callback();
  }
}

Use Arrow Function for Simplicity

For short, inline callbacks, arrow functions make code concise and clean.

setTimeout(() => {
  console.log("Hello after 1 second");
}, 1000);

Conclusion

Callback functions are a fundamental concept in JavaScript that power everything from simple timers to complex asynchronous workflows. Understanding how they work — and how to use them effectively — is key to writing clean, modular, and non-blocking code.

Whether you’re just starting out or revisiting JavaScript fundamentals, here’s what to take away:

  • Callbacks let you control the flow of your code after an operation completes.

  • They're crucial for handling async tasks like fetching data, responding to events, or waiting for timers.

  • By mastering custom callbacks, and understanding how they compare with Promises and async/await, you'll be better equipped to write scalable and maintainable JavaScript.

And while modern JavaScript favors Promises and async/await for most async operations, callbacks are still everywhere — especially in legacy code, event listeners, and Node.js APIs.

So, embrace callbacks — they’re more than just a stepping stone. They're a core building block of how JavaScript works.

1
Subscribe to my newsletter

Read articles from himanshu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

himanshu
himanshu