Skip to content
Atul's Blog
Go back

What the heck are callbacks?

Table of Contents

Open Table of Contents

What are callback functions?

A callback is a function passed into another function as an argument (like you pass variables) to be executed later.

For example:

function clickButton() {
  console.log("Clicked Button!");
}

div.addEventListener("click", clickButton);

Here, the addEventListener method takes a callback clickButton function and then calls it when div is clicked.

Why do we need callbacks

JS is a single-threaded, event-driven programming language.

Single Threaded

JS is a single-threaded language and only has a single call stack, that means it can only execute a single thing at a time. Slow operations, like I/O, network requests, etc. will block the main thread and you will have to wait for these operations to complete before you can execute anything else.

data = request(url); // makes an http request to the passed url (5s)
console.log(data) // logs the data

Let’s assume that after 5 secs we got the response from our http request. Imagine having to wait 5 secs before you can do anything else on your favorite website. This doesn’t sound like a good User experience.

We can solve this problem by using callbacks. Consider this code snippet:

function request(url, cb) {
  // Make http request to the url
  cb(response); // execute the passed callback with the response
}

console.log("Start");
request('https://www.google.com', callback);
console.log("End");

This program kicks off the request method and starts executing next line in the main program. It doesn’t block the main thread as it doesn’t wait for the response. Once it gets the response back from the server and call stack is empty then the callback cb will be executed with this response.

Event Driven Language

JS is an event-driven language. This means it can listen for and respond to events, while continuing to execute further code and without blocking its thread.

Without callbacks your event listeners will look something like this:

button.addEventListener("click") {
  console.log("Clicked Button");
}

Now all you can do on your website is Click a button and your website is waiting for you to do it. There’s no notion of asynchrony i.e. what would you like to do after someone clicks the button. You’re waiting for someone to do it now.

Again, you can resolve this via callbacks.

button.addEventListener("click", function() {
  console.log("Clicked button");
})

Now you can do all the other things on a website, as you have attached a callback to the event click on your button. Now your button knows what to do after someone clicks it. It isn’t waiting for you to click it now.

Write more generic functions

Array methods like map, reduce and filter are called higher order functions because they take another function (callback) as an argument, which makes them more generic.

For example:

function incrementByOne(numbers) {
  let results = [];
  for (const number of numbers) {
    results.push(number + 1);
  }

  return results;
}

let numbers = [3, 7, 10, 15];
console.log(incrementByOne(numbers)); // [4, 8, 11, 16]

The function incrementByOne takes a numbers array and returns a new array with all the numbers incremented by 1.

Let’s say we want to write a function which returns a new array with all the numbers getting doubled.

function double(numbers) {
  let results = [];
  for (const number of numbers) {
    results.push(2 * number);
  }

  return results;
}

let numbers = [3, 7, 10, 15];
console.log(double(numbers)); // [6, 14, 20, 30]

The above two functions have so much in common, lets extract the common functionality from both the function and specify the specific behaviour you want via callback. Then the above implementation can be simplified as:

function map(numbers, cb) {
  let results = [];
  for (const number of numbers) {
    results.push(cb(number));
  }

  return results;
}

function incrementByOne(number) {
  return number + 1;
}

function double(number) {
  return 2 * number;
}

let numbers = [3, 7, 10, 15];
console.log(map(numbers, incrementByOne)); // [4, 8, 11, 16]
console.log(map(numbers, double)); // [6, 14, 20, 30]

Synchronous Callbacks

The above implementation of map takes a callback and that callback is synchronous. This means that the function receiving the callback ( here its map) will have to wait for the callback (cb) execution to finish before it can resume its execution. This is called blocking. Hence, synchronous callbacks are blocking in nature. They will block the main thread and take it for themselves when they are invoked.

Also, synchronous callbacks are executed during the execution of the higher-order function that uses the callback. In the above implementation the callbacks incrementByOne and double are synchronous callbacks because they gets executed during the execution of the higher-order function map.

Asynchronous Callbacks

Asynchronous callbacks are non-blocking in nature. They are executed after the execution of the higher-order function that uses the callback.

We can simulate asynchronous callbacks via setTimeout function, which is an API provided by the browser.

console.log("Start");

// simulating asynchronous callback via setTimeout
setTimeout(() => {
  console.log("Asynchronous callback");
}, 1000);

console.log("End");

Instead of waiting for the setTimeout to finish, the execution moves to the next line and we get the following output:

Start
End
Asynchronous callback

This is helpful because we don’t have to wait for slow operations like I/O, network requests to complete, we can move the execution to the next stage and once the async (or slow) operation is complete, the corresponding callback code will execute.



Share this post: