Year End Sale: Get Upto 40% OFF on Live Training! Offer Ending in
D
H
M
S
Get Now
Callback Functions in JavaScript: A Comprehensive Guide

Callback Functions in JavaScript: A Comprehensive Guide

01 Oct 2024
Intermediate
511 Views
26 min read
Learn with an interactive course and practical hands-on labs

Free Javascript Course: Learn Javascript In 21 Days

JavaScript Callback Functions

Callback functions in JavaScript are used to handle asynchronous operations. Callbacks are executed after another function has finished execution.

In this JavaScript tutorial, we'll learn about callback functions, how they work, the need for callback functions, nested callbacks, the differences among callback functions, promises, Async/Await, applications of callbacks, etc.

Read More: JavaScript Interview Questions and Answers (Fresher + Experience)

What is a Callback Function?

A callback function is a function that is passed as an argument to another function and called after the primary function executes.

How Callbacks Work in JavaScript?

There are two basic steps involved:

  1. Passing the Function: The function you want to run after some operation is passed as an argument to another function.
  2. Executing the Callback: The main function executes the callback function at the appropriate time. It is executed after the completion of the main task or when an event occurs.

Example of a Callback Function in JavaScript


function greet(name, callback) {
  console.log(`Welcome to ${name}!`);
  callback();
}

function trainingSession() {
  console.log("We are going to start our JavaScript Programming Course!");
}

greet("ScholarHat", trainingSession);     

In the above code,

  • The greet() function takes a name and a callback function as arguments.
  • After greeting the trainees, it calls the callback function.

Output

Welcome to ScholarHat!
We are going to start our JavaScript Programming Course!      

Why do we need Callback Functions in JavaScript?

JavaScript is single-threaded, executing one operation at a time. It runs code sequentially in a top-down manner. For performing asynchronous operations in JavaScript, we require callback functions. Asynchronous actions are those that do not interfere with the main program execution but execute in the background.

Callback functions perform asynchronous actions synchronously. They allow a function to run just after the completion of the primary task. Callbacks are invoked as soon as an event occurs, making JavaScript an event-driven language.

Callback as an Arrow Function

You can write the callback function in the form of an Arrow function in Javascript introduced in ES6.

Example illustrating Callback as an Arrow Function


setTimeout(() => { 
    console.log("This message is shown after 4 seconds");
}, 4000);

In the above example, the setTimeout() function takes two arguments:

  1. a callback function
  2. delay time in milliseconds

The callback function here is an arrow function.

Output

This message is shown after 4 seconds

Handling Errors in Callbacks

An appropriate mechanism for passing error information is required to handle errors with callbacks. The simplest way is to add an error parameter to the callback function.


function task1(callback) {
    setTimeout(() => {
        if (Math.random() > 0.8) { // Simulate an error condition randomly
            callback(new Error("Error in Task 1"));
        } else {
            console.log("Task 1 completed");
            callback(null);
        }
    }, 1000);
}

function task2(callback) {
    setTimeout(() => {
        if (Math.random() > 0.8) { // Simulate an error condition randomly
            callback(new Error("Error in Task 2"));
        } else {
            console.log("Task 2 completed");
            callback(null);
        }
    }, 1000);
}

function task3(callback) {
    setTimeout(() => {
        if (Math.random() > 0.8) { // Simulate an error condition randomly
            callback(new Error("Error in Task 3"));
        } else {
            console.log("Task 3 completed");
            callback(null);
        }
    }, 1000);
}

function task4(callback) {
    setTimeout(() => {
        if (Math.random() > 0.8) { // Simulate an error condition randomly
            callback(new Error("Error in Task 4"));
        } else {
            console.log("Task 4 completed");
            callback(null);
        }
    }, 1000);
}

// Callback functions with error handling
function handleTask4(err) {
    if (err) {
        console.error(err.message);
        return;
    }
    console.log("All tasks completed");
}

function handleTask3(err) {
    if (err) {
        console.error(err.message);
        return;
    }
    task4(handleTask4);
}

function handleTask2(err) {
    if (err) {
        console.error(err.message);
        return;
    }
    task3(handleTask3);
}

function handleTask1(err) {
    if (err) {
        console.error(err.message);
        return;
    }
    task2(handleTask2);
}

// Start the task sequence
task1(handleTask1);

Each task function randomly generates an error or is completed successfully. If an error occurs, the callback is called with an Error object. Otherwise, it is called with null.

Output

Task 1 completed
Task 2 completed
Task 3 completed
Task 4 completed
All tasks completed

Nested Callbacks and Callback Hell in JavaScript

A nested callback, or callback hell, is one callback function inside another callback, which is again inside another, and so on. When multiple asynchronous operations depend on each other, deeply nested callbacks result. It becomes difficult to manage such nested callbacks when the application becomes large and complex.

Example of Callback Hell in JavaScript


function task1(callback) {
    setTimeout(() => {
        console.log("Task 1 completed");
        callback();
    }, 1000);
}

function task2(callback) {
    setTimeout(() => {
        console.log("Task 2 completed");
        callback();
    }, 1000);
}

function task3(callback) {
    setTimeout(() => {
        console.log("Task 3 completed");
        callback();
    }, 1000);
}

function task4(callback) {
    setTimeout(() => {
        console.log("Task 4 completed");
        callback();
    }, 1000);
}

// Using nested callbacks to create callback hell
task1(() => {
    task2(() => {
        task3(() => {
            task4(() => {
                console.log("All tasks completed");
            });
        });
    });
});

Here, each task function accepts a callback function to be called when the task is completed. The nested structure of callbacks results in deeply nested code, which is hard to read and maintain.

Output

Task 1 completed
Task 2 completed
Task 3 completed
Task 4 completed
All tasks completed

Ways of Handling Callback Hell

There are four ways to solve the problem of nested callbacks:

1. Write comments

Comments can improve the readability and maintainability of nested callbacks.

Example of Using Comments to Handle Nested Callbacks


function task1(callback) {
    setTimeout(() => {
        console.log("Task 1 completed");
        callback();
    }, 1000);
}

function task2(callback) {
    setTimeout(() => {
        console.log("Task 2 completed");
        callback();
    }, 1000);
}

function task3(callback) {
    setTimeout(() => {
        console.log("Task 3 completed");
        callback();
    }, 1000);
}

function task4(callback) {
    setTimeout(() => {
        console.log("Task 4 completed");
        callback();
    }, 1000);
}

// Initiating task sequence
task1(() => {
    // Task 1 completed, starting Task 2
    task2(() => {
        // Task 2 completed, starting Task 3
        task3(() => {
            // Task 3 completed, starting Task 4
            task4(() => {
                // Task 4 completed, all tasks done
                console.log("All tasks completed");
            });
        });
    });
});

Comments are added before the callback invocation inside each task to indicate which task is being started and which one is just completed. This clarifies the flow of operations and helps in understanding the sequence of asynchronous tasks.

Output

Task 1 completed
Task 2 completed
Task 3 completed
Task 4 completed
All tasks completed

2. Using Promises

A promise in JavaScript is an object that will return a value in the future.

To convert callbacks into promises, we have to create a new promise for each callback. On a successful callback, we can resolve the promise; otherwise, we can reject it in case of failure.

Example of Using Promises to Handle Nested Callbacks


function task1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Task 1 completed");
            resolve();
        }, 1000);
    });
}

function task2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Task 2 completed");
            resolve();
        }, 1000);
    });
}

function task3() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Task 3 completed");
            resolve();
        }, 1000);
    });
}

function task4() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Task 4 completed");
            resolve();
        }, 1000);
    });
}

// Using Promises to chain tasks
task1()
    .then(() => task2())
    .then(() => task3())
    .then(() => task4())
    .then(() => {
        console.log("All tasks completed");
    })
    .catch((err) => {
        console.error("An error occurred:", err);
    });

Each task function returns a promise that resolves after a delay of 1 second, simulating an asynchronous operation. The tasks are executed in sequence by chaining the promises.

Output

Task 1 completed
Task 2 completed
Task 3 completed
Task 4 completed
All tasks completed

3. Using Async/await

Asynchronous functions contain the async keyword and always return a promise. The await keyword pauses the function execution until the promise is resolved. This helps your asynchronous code look synchronous.

Example of Using Async/await to Handle Nested Callbacks.


function task1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Task 1 completed");
            resolve();
        }, 1000);
    });
}

function task2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Task 2 completed");
            resolve();
        }, 1000);
    });
}

function task3() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Task 3 completed");
            resolve();
        }, 1000);
    });
}

function task4() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Task 4 completed");
            resolve();
        }, 1000);
    });
}

async function runTasks() {
    try {
        await task1();
        await task2();
        await task3();
        await task4();
        console.log("All tasks completed");
    } catch (err) {
        console.error("An error occurred:", err);
    }
}

// Running tasks using async/await
runTasks();

The async function runTasks() declares an asynchronous function runTasks to run the tasks sequentially using await. The try-catch block is used for error handling to catch any errors that might occur during the tasks' execution.

Output

Task 1 completed
Task 2 completed
Task 3 completed
Task 4 completed
All tasks completed

4. Spliting callbacks into smaller functions

When you split the nested callbacks into different functions, each handles a specific step in the task sequence.

Example illustrating splitting callbacks into smaller functions to Handle Nested Callbacks.


function task1(callback) {
    setTimeout(() => {
        console.log("Task 1 completed");
        callback();
    }, 1000);
}

function task2(callback) {
    setTimeout(() => {
        console.log("Task 2 completed");
        callback();
    }, 1000);
}

function task3(callback) {
    setTimeout(() => {
        console.log("Task 3 completed");
        callback();
    }, 1000);
}

function task4(callback) {
    setTimeout(() => {
        console.log("Task 4 completed");
        callback();
    }, 1000);
}

// Callback functions
function handleTask4() {
    console.log("All tasks completed");
}

function handleTask3() {
    task4(handleTask4);
}

function handleTask2() {
    task3(handleTask3);
}

function handleTask1() {
    task2(handleTask2);
}

// Start the task sequence
task1(handleTask1);

In the above code, the callback functions handle the completion of each task and trigger the next task in the sequence. The task sequence is started by calling task1 with handleTask1 as its callback.

Output

Task 1 completed
Task 2 completed
Task 3 completed
Task 4 completed
All tasks completed

Callback Functions vs. Promises vs. Async/Await

ParametersCallbacksPromisesAsync/Await
Primary applicationManaging asynchronous operationshandling multiple asynchronous operations in a more structured mannerSynchronizing asynchronous code
ComplexityLow readability due to callback hellmore readableclean and much readable asynchronous code
Performancegoodoverhead due to the promise creationmore overhead compared to promises
Error Handlingseparate error handlingerror handling with .catch() methoderror handling using try-and-catch blocks
Debugging complexitydifficult debuggingeasy to debugeasiest to debug
Browser and EnvironmentJavaScript environmentsModern web browsers and Node.jsModern web browsers and Node.js

Use Cases for JavaScript Callbacks

  • Asynchronous event handling: Callbacks can efficiently handle non-blocking events like network requests, timers, or user interactions.
  • Increasing code modularity: Callbacks enhance code readability and understanding by organizing the code into various functions, each with callbacks as one of its parameters.
  • Higher-order functions: The programmer has the flexibility to use callbacks to create higher-order functions that can manipulate or extend the behavior of other functions.
  • Array Operations: Array methods in JavaScript like map, filter, and reduce take callbacks to perform operations on array elements.
  • Error Handling: Callbacks can be used to handle errors in asynchronous operations.
  • Creating Custom Asynchronous Functions: You can create custom asynchronous functions using callbacks to perform multiple operations.
Summary

Callback in JavaScript is a fundamental concept for performing asynchronous operations. We saw the creation and working of the callback function with examples. Though callbacks are very useful, they are also prone to errors and complexity. To resolve this issue, we saw the two alternatives, promises and async/await. You need to understand all these concepts thoroughly to simplify your code and increase the program efficiency. For the implementation of the learned concepts, consider our JavaScript Programming Course.

Take our Javascript skill challenge to evaluate yourself!

In less than 5 minutes, with our skill challenge, you can identify your knowledge gaps and strengths in a given skill.

GET FREE CHALLENGE

Share Article
About Author
Shailendra Chauhan (Microsoft MVP, Founder & CEO at ScholarHat)

Shailendra Chauhan, Founder and CEO of ScholarHat by DotNetTricks, is a renowned expert in System Design, Software Architecture, Azure Cloud, .NET, Angular, React, Node.js, Microservices, DevOps, and Cross-Platform Mobile App Development. His skill set extends into emerging fields like Data Science, Python, Azure AI/ML, and Generative AI, making him a well-rounded expert who bridges traditional development frameworks with cutting-edge advancements. Recognized as a Microsoft Most Valuable Professional (MVP) for an impressive 9 consecutive years (2016–2024), he has consistently demonstrated excellence in delivering impactful solutions and inspiring learners.

Shailendra’s unique, hands-on training programs and bestselling books have empowered thousands of professionals to excel in their careers and crack tough interviews. A visionary leader, he continues to revolutionize technology education with his innovative approach.
Accept cookies & close this