Understanding JavaScript Callback Functions

Alok RaturiAlok Raturi
6 min read

Callback functions are one of the main pillars that support the asynchronous nature of JavaScript. With callbacks, developers can specify a piece of code to execute when a particular event occurs. Besides events, callback functions are crucial for handling asynchronous tasks like API calls, setTimeout(), and setInterval() functions.

What is a Callback Function

A callback function in JavaScript is a function passed to another function as an argument and executed after a specific event is triggered or some operation is completed.

A callback function that is passed to a function can be executed in that function body at any point in that function. It is the responsibility of the developer to call the callback function with the correct argument in the function body.

A callback function can be used to perform a synchronous operation or a asynchronous operation.

Example

In JavaScript, we can pass any type of function into other functions as arguments because functions are first-class citizens. The function we pass can be an anonymous function, an arrow function, or a regular function. Below are a few examples of callback functions in JavaScript. Callbacks can be used for both asynchronous and synchronous tasks.

// callback for input validation
function outerFun(a, cb){
    if(cb(a)){
        console.log("Callback function returned true")
        // Further processing on a
    }else{
        console.log("Callback function returned false")
    }
}

function checkLength(name){
    return name.length==4
}

outerFun("alok",checkLength)

The example above shows how callbacks can be used to perform a synchronous task, making the code more modular.

document.getElementById("click",()=>{
    console.log("Button Clicked");
});

The event handler callback can be either synchronous or asynchronous. Once the event is triggered, the callback will be executed immediately.

setTimeout(function(){
    console.log("Timer Expired")
},2000)

The timer function's callbacks are always asynchronous. Similarly, AJAX requests are also asynchronous.

Use cases of Callbacks:

There are many uses of callbacks. Some of them are:

  1. Event handling: Callback functions are used for event handling in JavaScript. A callback is executed whenever an event is triggered.

  2. AJAX Requests: Callback functions are used to process the data from AJAX requests.

  3. JavaScript’s Higher-order functions: Many JavaScript functions take callbacks as input and return some data based on that callback function. Examples include map, filter, reduce, and find.

  4. Timer Functions: Timer functions like setTimeout and setInterval also take a function and execute it after a certain time interval.

  5. In Promises: Promise methods (.then and .catch) also take callbacks as arguments and execute them once the promise is resolved.

Challenges with Callbacks

There are two challenges associated with callbacks. These challenges make the code difficult to maintain and debug. They are:

  1. Callback Hell

    • Callback hell happens when we pass a callback function to another function, and that callback function calls another function that also takes a callback, and so on.

    • This creates a situation where multiple nested callback functions form a complex structure that is hard to read, maintain, and debug.

    • This structure is also known as the pyramid of doom.

    • It makes our code complex and unreadable, harder to debug and maintain, and results in poor code quality.

    • Below is an example of callback hell. In the code below, the greet function takes a callback function, which then calls another function askStatus, which also takes a callback, and so on, leading to callback hell.

    •         function greet(callback,name) {
                setTimeout(function () {
                  callback(`Hello ${name}, `);
                }, 1000);
              }
      
              function askStatus(data, callback) {
                setTimeout(function () {
                  callback(data + "  What are you doing? ");
                }, 1000);
              }
      
              function goodBye(data, callback) {
                setTimeout(function () {
                  callback(data + "  Take Care. Good Bye.  ");
                }, 1000);
              }
      
              function callback(a) {
                askStatus(a, function (b) {
                  goodBye(b, function (c) {
                      console.log(c);
                  });
                });
              }
              greet(callback,"alok");
      
  2. Inversion of Control

    • Whenever we pass a callback function to another function (caller) as an argument, it is up to that function to execute the callback correctly. The caller has the power to run the function as many times as it wants.

    • When the caller is running the callback function, it is handing over control to the callback, meaning the caller function has to wait until the callback finishes.

    • This inversion of control can create challenges in tracking the execution flow and debugging the program.

Solution

Callback functions in JavaScript are essential for handling asynchronous operations, but they can introduce many challenges that affect code quality. To address these challenges, JavaScript offers features that help reduce the problems associated with callbacks.

Use Promises:
  • A promise object represents the eventual completion of an asynchronous operation.

  • Promises come with a .then() method that takes a callback as an argument. We can chain as many .then() methods as we want, and the callback inside .then() will execute once the promise and the .then() method above it are resolved.

  • Promises also come with a .catch() method, which takes a callback and is executed if there is an error during promise resolution.

  • Below is the code that executes the above callback hell code using promises.

function greet(name) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(`Hello ${name}, `);
    }, 1000);
  });
}

function askStatus(data) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(data + "  What are you doing? ");
    }, 1000);
  });
}

function goodBye(data) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(data + "  Take Care. Good Bye.  ");
    }, 1000);
  });
}

greet("Alok")
  .then(askStatus)
  .then(goodBye)
  .then((data) => console.log(data))
  .catch((err) => console.log(err));

For a clearer understanding of promises, check out this blog by Rahul Singh Rautela.

Use Async/Await:

  • The async keyword in JavaScript is used to declare a function as an asynchronous function, which always returns a promise.

  • The await keyword can only be used inside an async function. It is used to wait for the result of an asynchronous task. The function execution is paused until the result from the async task is obtained.

  • Below is the code that executes the above callback hell example using async/await.

function greet(name) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(`Hello ${name}, `);
    }, 1000);
  });
}

function askStatus(data) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(data + "  What are you doing? ");
    }, 1000);
  });
}

function goodBye(data) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(data + "  Take Care. Good Bye.  ");
    }, 1000);
  });
}

async function main() {
  try {
    const greeting = await greet("Alok");
    const status = await askStatus(greeting);
    const finalMsg= await goodBye(status);
    console.log(finalMsg);
  } catch (err) {
    console.log(err);
  }
}
main();

In addition to these solutions, we should try to modularize our code to avoid nesting callbacks. We should also implement proper error handling techniques to manage errors in callbacks more efficiently.

For a better understanding of async/await, check out this blog by Madhur Gupta.

Conclusion

Callback functions enable asynchronous programming in JavaScript. They are used extensively, especially in event handling, network requests, and timer functions. By understanding callbacks in JavaScript, we can write more modular, flexible, and asynchronous code. However, callbacks come with challenges like callback hell and inversion of control, making the code error-prone. By using promises and async/await, we can overcome these challenges and make our code easier to debug, maintain, and read. Overall, it is essential for a JavaScript developer to have a good understanding of callbacks.

Thank you for your time! Please let me know if there are any opportunities for improvement in this blog or any additions I can make in the comment section.

11
Subscribe to my newsletter

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

Written by

Alok Raturi
Alok Raturi