Understanding JavaScript Callbacks: A Beginner's Guide


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
You place your order (the initial request) with the waiter.
The waiter takes your order to the kitchen (the asynchronous operation).
While waiting, you can continue with other activities (not being blocked by the operation).
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
Feature | Callbacks | Promises | Async/Await |
Syntax | Nested functions passed as arguments | Chain of .then() and .catch() methods | Uses async function declaration and await keyword |
Introduced | Early JavaScript | ES6 (2015) | ES8 (2017) |
Error Handling | Custom error parameters or try/catch blocks around entire operation | .catch() method | Standard try/catch blocks |
Code Structure | Can lead to "callback hell" with multiple levels of nesting | Flattened chain but still requires method chaining | Looks like synchronous code, minimal nesting |
Readability | Poor with multiple callbacks | Better than callbacks | Best, resembles synchronous code |
Debugging | Difficult (stack traces are less helpful) | Improved | Best (clear stack traces) |
Sequential Operations | Nested callbacks | Chain of .then() calls | Simple sequence of await statements |
Parallel Operations | Custom implementation | Built-in with Promise.all() , Promise.race() | Use Promise.all() with await |
Underlying Mechanism | Functions | Promise objects | Promises (syntactic sugar) |
Control Flow | Manual handling required | Built-in promise methods | Looks like standard control flow |
Cancelability | Can be implemented | Not built-in (requires workarounds) | Not built-in (requires workarounds) |
Example | getData(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.
Subscribe to my newsletter
Read articles from himanshu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
