DEV Community

Cover image for Callback functions in Javascript explained
Santiago Rincón
Santiago Rincón

Posted on

Callback functions in Javascript explained

Imagine you are leading a development team responsible for creating a website, and you've found that the only remaining task for your team is to add a logo to the website.

However, as a team of developers, you guys lack the necessary skills to design a professional logo. Thus, you must reach out to the design team to get the piece of art. Once the design team has provided the logo, you can then assign the task of adding it to the website to one of your developers.

It's currently 2 PM on a Friday and you're eagerly waiting for the design team to provide the necessary design. Nonetheless, as time passes, you become increasingly frustrated with the delay. Finally, at 4:30 PM, the design team delivers the art, and now you're able to assign the task to a developer. However, due to the delay, it is now 6 PM, and the team is frustrated that they could not complete the work earlier.

Now imagine instead of waiting for the design team to deliver before assigning the task to a developer, you could have asked the design team to provide the logo directly to the developer responsible for adding it to the website. That could have saved you time.

This way, the developer could have started working on the container and had everything ready to add the design as soon as it was received. Everyone's happy. This is a callback function, and it's such a magical thing.

So what exactly are callback functions?

In simple terms, a callback function is just a function you can pass to another function for it to be invoked after performing another action.

The most interesting thing about them is that we use them all the time without even realizing it. Here are some examples:

Event handling

In web development, callback functions are commonly used to handle events: mouse clicks, key presses, form submissions, and so on. Event listeners are attached to HTML elements and triggered when the corresponding event occurs. The callback function is then executed, allowing the program to respond to the event.

I'm sure you've used something like this.

const button = document.querySelector("button");

button.addEventListener("click", function() {
  console.log("Button clicked!");
});
Enter fullscreen mode Exit fullscreen mode

Asynchronous operations

Callback functions are often used in JavaScript to handle asynchronous operations, for example: making an HTTP request. The callback function is passed to the asynchronous function, and it is called when the data is fetched.

fetch("https://pokeapi.co/api/v2/pokemon/ditto")
  .then(function(response) {
    // callback function executed when the HTTP request is complete
    return response.json();
  })
  .then(function(data) {
    // callback function executed when the response is converted to JSON
    console.log(data);
  });
Enter fullscreen mode Exit fullscreen mode

Array iteration

The Array.prototype.forEach(), Array.prototype.map(), etc. methods are examples of built-in JavaScript functions that take a callback function as an argument. These functions are used to iterate through an array and perform some operation for each element.

const numbers = [1, 2, 3, 4, 5];

// forEach method with callback function
numbers.forEach(function(number) {
  console.log(number);
});

// map method with callback function
const squaredNumbers = numbers.map(function(number) {
  return number * number;
});
Enter fullscreen mode Exit fullscreen mode

In all of these examples, callback functions allow programs to be more flexible and responsive to changing conditions. They allow us to write code that can respond to events or data as they become available, and provide a way to separate concerns in our code, making it more modular, scalable, and easier to maintain.

The best part of it is that you can implement them yourself. Let's take Array.prototype.map() to see calback functions in detail. This method takes a callback function as an argument and returns a new array with the result of calling the callback function on each element of the original array.

const numbers = [1, 2, 3, 4, 5];

const squaredNumbers = numbers.map(function(number) {
  return number * number;
});

console.log(squaredNumbers); // Output: [1, 4, 9, 16, 25]
Enter fullscreen mode Exit fullscreen mode

In this example, we have an array of numbers, and we want to create a new array with the squared values of each number. We can achieve this by calling the map() method on the numbers array and passing a callback function that takes a single argument (the current element of the array) and returns the squared value of that element.

The map() method then iterates through each element of the original array and applies the callback function to each element. The result of each call to the callback function is stored in a new array, which is returned once the iteration is done.

But, how does it work internally? Let's see a simplified example of how the map() method works.

Array.prototype.map = function(callback) {
  const newArray = []; // create a new array to store the result
  for (let i = 0; i < this.length; i++) { // iterate over each element of the original array
    const result = callback(this[i], i, this); // call the callback function on each element
    newArray.push(result); // add the result to the new array
  }
  return newArray; // return the new array with the results
};
Enter fullscreen mode Exit fullscreen mode

In this implementation, we define the map() method on the Array.prototype object, which allows us to call the method on any array object. The callback parameter is the function that will be called on each element of the array.

Inside the method, we create an empty array newArray to store the results of calling the callback function on each element. We then use a for loop to iterate over each element of the original array.

On each iteration, we call the callback function with three arguments: the current element of the array this[i], the index of the current element i, and the original array this. The callback function can use these arguments to transform the current element of the array and then return the transformed value.

We then add the result of the callback function to the newArray using the push() method, and finally, we return the newArray with the results of calling the callback function on each element of the original array.

Now let's try to turn the metaphor at the beginning of this post into code with and without callback functions.

With callback function

// Example with callback functions for website development process

// Assume we have a function that represents the development team
function developmentTeam() {
  console.log("Development team is ready to add the illustration to the website");
}

// Assume we have a function that represents the design team
function designTeam(callback) {
  console.log("Design team is working on the illustration...");
  setTimeout(function() {
    console.log("Design team finished the illustration!");
    callback();
  }, 1500); // Simulate a delay of 1.5 seconds to complete the illustration
}

// Assume we have a function that represents the implementation task
function implementationTask() {
  console.log("Developer added the illustration to the website");
}

// Assume we want to coordinate between the development and design teams to complete the website
developmentTeam();
designTeam(implementationTask);
Enter fullscreen mode Exit fullscreen mode

In this example, we have three functions: developmentTeam(), designTeam(callback), and implementationTask(). The first function represents the development team, while the second function represents the design team. The designTeam(callback) function takes a callback function as an argument, which represents the implementation task that the development team needs to perform.

Inside the designTeam(callback) function, we simulate a delay of 1.5 seconds to represent the time it takes for the design team to complete the design.

Once the design is completed, we call the callback function to notify the development team that they can proceed with the implementation task. Finally, we call developmentTeam() to indicate that the development team is ready to add the illustration to the website.

By using a callback function, we can ensure that the implementation task is only executed once the design team has completed the illustration. This helps to streamline the development process and avoid frustration on the development team.

Now without callback function

// Example without callback functions for website development process

// Assume we have a function that represents the development team
function developmentTeam() {
  console.log("Development team is ready to add the illustration to the website");
}

// Assume we have a function that represents the design team
function designTeam() {
  console.log("Design team is working on the illustration...");
  setTimeout(function() {
    console.log("Design team finished the illustration!");
    implementationTask();
  }, 1500); // Simulate a delay of 1.5 seconds to complete the illustration
}

// Assume we have a function that represents the implementation task
function implementationTask() {
  console.log("Developer added the illustration to the website");
}

// Assume we want to coordinate between the development and design teams to complete the website
developmentTeam();
designTeam();
Enter fullscreen mode Exit fullscreen mode

In this example, we have the same three functions: developmentTeam(), designTeam(), and implementationTask(). However, instead of passing a callback function to designTeam(), we call implementationTask() directly from within designTeam() once the deign is complete.

While this approach may work in some cases, it can lead to problems if the implementation task depends on other tasks that need to be completed before it. In such cases, it may be more difficult to manage the dependencies and ensure that tasks are executed in the correct order.

In general, using callback functions can help you simplify the development process and make it easier to manage complex dependencies between functions.

In conclusion, callback functions are an essential feature in JavaScript that allows developers to write asynchronous, event-driven code, among other things. By passing a function as an argument to another function, the callback function can be executed once the first function has completed its task. This allows us, developers, to write more efficient and maintainable code, as they can avoid blocking the execution of code while waiting for long-running operations to complete.

Callback functions are used extensively for many purposes, like handling user input, making HTTP requests, and working with timers. They are also commonly used in modern web development frameworks, such as Express and React, to handle asynchronous operations.

While callback functions can be a powerful tool in a developer's toolbox, it's essential to use them carefully and understand the potential pitfalls. For example, callback functions can lead to callback hell, where the code becomes difficult to read and manage due to excessive nested callbacks. Developers should also consider using other techniques, such as promises or async/await, to manage complex dependencies between functions.

Overall, callback functions are a crucial part of JavaScript, and every developer should understand how to use them effectively to write efficient, scalable, and maintainable code.

Top comments (0)