Back to posts
6 min read

Asynchronous Javascript 2 - Callback

The simplest and sufficient definition of a Callback function is “A function that is used as an argument for another function, which will later be called by that function to complete a task”.

function makeCoffee(callback) {
  console.log("brewing delicious coffee...");
  callback();
}

function getCoffee() {
  console.log("Here is your coffee ☕");
}

makeCoffee(getCoffee);

In the code above, the getCoffee function is used as an argument for the makeCoffee function which will be called after a certain process. getCoffee here is a callback.

If you’ve been learning Javascript for quite a while, you’ve probably often encountered callbacks without realizing it. When using timeouts, or operations with arrays.

const arr = ["halo", "darkness"];
function callback(str) {
  return str.toUpperCase();
}
const capitalized = arr.map(callback);

// setTimeout
function callback2() {
  console.log(capitalized);
}
setTimeout(callback2, 100); // ['HALO', 'DARKNESS']

Synchronous vs Asynchronous Callback

Callbacks can be called either synchronously or asynchronously, and distinguishing between the two is important when analyzing side-effects (the result of a process that changes a state, such as changing a variable value) of executed code. Look at the following code

let num = 1;

function tambah(callback) {
  callback()
}

tambah(() => {
  num = 2;
});

console.log(num);

If the Callback from suatuFungsi is called synchronously, then the output of num is 2 but if asynchronously, then the result is 1, because the variable num doesn’t have a new value yet.

Callback for Event listener

Event listeners that are commonly used to “listen” to everything that happens in the DOM, also accept a Callback to be called when an “Event” runs. The code below shows a Callback that is called every time the mouse cursor moves.

window.addEventListener('pointermove', (event) => {
  const container = document.querySelector('#data');

  container.innerText = 'Koordinat X: ' + event.clientX + ' • Koordinat Y: ' + event.clientY;
});

Callback for HTTP request

One process that has uncertain execution time is HTTP request, which is an interaction process with a server performed by a client (e.g. browser) to get or send data.

Here is an example code using AJAX to perform an HTTP request to the fake API service jsonplaceholder.

function getData(url, callback) {
  const request = new XMLHttpRequest();

  request.addEventListener("load", () => {
    if (request.status >= 200 && request.status < 300) {
      callback(undefined, request.responseText);
    } else {
      callback("Error", undefined);
    }
  });

  request.open("GET", url);
  request.send();
}

getData("https://jsonplaceholder.typicode.com/users", (err, data) => {
  if (err) {
    console.log(err);
  } else {
    document.getElementById("display").innerHTML = data
  }
});

In the code above, the getData function will make a request to the API to get user data. The getData function has two parameters, namely url for the API URL and callback which will be called with the result data whether successful or error.

  • The url parameter is the endpoint URL that will be accessed.
  • The callback parameter is a function that will be executed after the request is complete. This callback has two arguments: err for error (if any), and data for data received from the server.

Using callbacks in handling asynchronous operations has the weakness of potential callback hell, where callbacks are stacked too deeply so that the code becomes difficult to read and manage. To overcome this, JavaScript introduced Promise and async/await.

Callback hell

Callback hell is a condition where a Callback has many other callbacks inside it, wrapping each other, so that the existing code becomes difficult to read and prone to bugs.

Using the previous AJAX example, a scenario can be created where the data requested from the server is interconnected, thus creating callback hell.

Suppose to get data for a user who made a post where that post contains comments containing hate speech. Then, the steps that need to be taken to get that user’s data are:

  • get comment data containing hate speech
  • search for post data with that comment
  • get user data who made the post

Here is the code to perform the steps above:

function getData(url, callback) {
  const request = new XMLHttpRequest();

  request.addEventListener("load", () => {
    if (request.status >= 200 && request.status < 300) {
      const data = JSON.parse(request.responseText);
      callback(undefined, data);
    } else {
      callback("Error", undefined);
    }
  });

  request.open("GET", url);
  request.send();
}

const baseUrl = "https://jsonplaceholder.typicode.com";

// mendapatkan data komentar
getData(baseUrl + "/comments/1", (err, data) => {
  if (err) {
    console.log(err);
  } else {
    const postId = data.postId;
    // mendapatkan data post spesifik
    getData(baseUrl + "/posts/" + postId, (err, data) => {
      if (err) {
        console.log(err);
      } else {
        const userId = data.userId;
        // mendapatkan data user
        getData(baseUrl + "/users/" + userId, (err, data) => {
          if (err) {
            console.log(err);
          } else {
            document.getElementById("display").innerHTML = JSON.stringify(data, null, 4);
          }
        });
      }
    });
  }
});

Notice the use of getData functions that wrap each other in the code above. At a glance, it’s quite difficult to capture the process of requesting and displaying data. The code above is actually still quite simple because there is no data manipulation or serious error handling.

Imagine if the process of requesting data from the server is not just 3, but 5 or 7, how difficult it will be to manage the code later. So this condition is called callback hell or pyramid of doom, because on the left side of the code a pyramid will be formed that gets more terrifying as it gets higher.

Bonus: Callback for more dynamic programming

Besides being useful for dealing with asynchronous situations, callbacks are also useful for creating more dynamic abstractions. Look at the following code

function validateString(str, rules) {
  // membersihkan string
  const cleanString = str.trim();

  // cek apakah string kosong
  if (rules === "required") {
    if (cleanString.length === 0) {
      return false;
    }
  }

  // cek apakah string sepenuhnya alphabet
  if (rules === "alphabet") {
    if (/^[A-Za-z ]+$/.test(cleanString)) {
      return false;
    }
  }

  // cek maksimal karakter
  if (rules === "max") {
    if (cleanString.length > 10) {
      return false;
    }
  }

  // jika semua validasi oke
  return true;
}

The code above is a simple function to validate characters in a string. Functionally, the validateString function is fine, it can validate as desired. However, what if you want to add other validations? if more and more conditions are added, the function will certainly become bigger and monotonous.

With callbacks, the code above can be made more dynamic and improve its readability. Look at the code modification below:

function validateString(str, validator) {
  // membersihkan string
  const cleanString = str.trim();

  // melakukan validasi
  return validator(cleanString);
}

function isRequired(str) {
  return str.trim().length > 0;
}

function isAlphabetic(str) {
  return /^[A-Za-z ]+$/.test(str);
}

function maxLength(str, maxLength) {
  return str.length <= maxLength;
}

// penggunaan
validateString("Hello", isRequired);
validateString("Hello", isAlphabetic);
validateString("Hello", (str) => maxLength(str, 5));

References