DEV Community

Cover image for Async Functions - Chapter 1: Callback
skaytech
skaytech

Posted on • Edited on • Originally published at blog.skay.dev

Async Functions - Chapter 1: Callback

Introduction

I think Async is one of the most powerful yet not-so-easy concepts to grasp in JavaScript. Especially if you are new to programming or if even if you have prior experience with other programming languages such as Java or Dot Net.

I am going to make this into a three-part series comprising of the following topics:

Async Series Links
Part 1 Callback
Part 2 Promises
Part 3 Async/Await

The reason being, each of those topics is vast and I think it would make sense for you to pause at the end of each topic and take it all in.

Asynchronous Execution

Let us first understand what an asynchronous activity is. Let us assume, you need to go to a laundromat to wash your clothes and to the bank to withdraw some money.

You first head over to laundromat and put your clothes to wash and it says the wash/dry cycle will take about an hour. Instead of waiting in the laundromat for an hour, you decide to go to the bank instead which is a 20-min drive and make the withdrawal. You realize you are still left with some time and make a quick stop at the supermarket and pick up a few things. Finally, at the end of the hour, you return to the laundromat to collect your clothes.

In the above example, while the clothes were being washed, you still proceeded to do other activities. This is precisely what asynchronous activity is all about. One program execution does not happen sequentially to other program executions.

Higher-Order Function & Callback

In the example shown below, we have a simple function returning the sum of two numbers.

//Function returns the sum of two numbers
const add = function(a, b) {
    return a + b;
}

//Output the sum of two numbers
console.log(add(2, 3));
Enter fullscreen mode Exit fullscreen mode

A cool thing that you can do in JavaScript language is that you can pass other functions to your function. If you would like to revisit JavaScript functions basics, you can read them over here.

//Function returns the sum of two numbers
const add = function (a, b) {
  return a + b;
};

const multiply = function (a, b) {
  return a * b;
};

const calculate = (funcParam) => (a, b) => {
  return funcParam(a, b);
};

//PASSING FUNCTION AS AN ARGUMENT
//Pass the 'add' function to the 'calculate' function
const sum = calculate(add);
console.log(sum(2, 3)); //Output the sum of two numbers -> 5

//PASSING FUNCTION AS AN ARGUMENT
//Pass the multiply function to the 'calculate' function
const multi = calculate(multiply);
console.log(multi(2, 3)); //Output the multiplication of two numbers -> 6
Enter fullscreen mode Exit fullscreen mode

Things to note:

  • The function add is passed as an argument to the calculate function and assigned to the 'sum' variable. Likewise, the function 'multiply' is passed as an argument in the next line and assigned to the variable 'multi'.
  • The parameter 'funcParam' of the 'calculate' function holds a reference to either 'add' or 'multiply' function based on what is passed in while invoking the function.

NOTE: The function 'sum' or 'multi' is known as the 'higher-order function' and the function 'add' or 'multiply' that is passed as an argument is known as the 'callback'.

Using the above semantics, the same example can be demonstrated as shown below:

const callback = function(a, b) {
    return a + b;
}

const higherOrderFunction = (callback) => (a, b) => {
    return callback(a, b);
}
Enter fullscreen mode Exit fullscreen mode

Callbacks are used everywhere in JavaScript. Array-based functions such as a map, filter, sort, etc. use callbacks and if you are already using JavaScript most likely you are already using them without realizing that they are callbacks.

How does Callback help?

In most of the real-life applications we build, the UI will have to wait to fetch data from the backend, while the user continues to interact with the web application. This is exactly the use-case for callback functions.

Let us look at an example of a function making an external API call:

//Define the Github User ID
const userId = 'skaytech';

/*
Function to fetch data using XMLHTTPRequest
The function accepts a callback to invoke upon the success
*/
const fetchData = function(userId, callbacks, callback2) {
    //Initialize xhr to a new XMLHttpRequest object 
    const xhr = new XMLHttpRequest();

    // Define the parameters to call an External API
    // Calling the Github getUsers API by userId
    // Params are - HTTP Method name, URL, Async (true/false)
    // When the third param is 'true', it means it's an asynchronous request
    xhr.open(
      'GET', `https://api.github.com/users/${userId}`, true);

    //The onload method will execute when a response has been received from external API
    xhr.onload = function() {
        //Checking for a response of 200 (It's a success (OK) response)
        if (xhr.status === 200) {
            //On success - invoke the callback method passed to the function
                        //In this example - displayUserPicture function will be run
            callback1(xhr.responseText);
        } else {
            //On Error - invoke the onError method and pass the HTTP status
            callback2(xhr.status);
        }
    }

    //Upon Send the XMLHttpRequest will actual be processed
    //This is the method that actually triggers the API call
    xhr.send();

}

//UI method to display the picture of Github User
function displayUserPicture(response) {
    const data = JSON.parse(response);
    const imgUrl = data.avatar_url;
    document.querySelector('#userimg').setAttribute('src', imgUrl);
}

//UI method to display Error if the Github User does not exits
function onError(status) {
             document.querySelector('#userimg').style.display = 'none';
  document.querySelector('#errorDiv').textContent = `Error Status: ${status}`;
}

//Invoke the fetch data function
//Params - userId & displayUserPicture is the callback function
fetchData(userId, displayUserPicture, onError);
Enter fullscreen mode Exit fullscreen mode

Things to note:

  • In the above example, I have used the XMLHttpRequest which is used to make external API calls. This is one of the earliest methods in JavaScript to make API requests. You can read about them over here.
  • The function 'fetchData' accepts the callback methods 'displayUserPicture' and 'onError'.
  • If the HTTP response status is 200 then the function 'displayUserPicture' will be executed or else the function 'onError' will be executed.
  • Here the UI update method will not be invoked until the data from external API is available. If it's successful, the GitHub user's image will be displayed and on error, the error status will be displayed on the UI.

You can find play around with the above code over here.

Callback Hell

In the above example, we had seen that the callbacks are separate methods that are invoked from within the main function. Let us look at an example where instead of calling a separate function, you are nesting the function calls.

//Define the Github User ID
const userId = 'skaytech';

/*
Function to fetch data using XMLHTTPRequest
The function accepts a callback to invoke upon the success
*/
const fetchData = function(userId, callback1, callback2) {
    const xhr = new XMLHttpRequest();

    xhr.open(
        'GET', `https://api.github.com/users/${userId}`, true);

    xhr.onload = function() {
        if (xhr.status === 200) {
            //Parse the incoming response to JSON object
            const data = JSON.parse(response);
            //Fetch the user's followers URL
            const followersUrl = data.followers_url;

            //Create another XMLHttpRequest
            const xhr1 = new XMLHttpRequest();

            xhr1.open('GET', followersUrl, true);
            xhr1.onload = function() {
                if (xhr.status === 200) {
                    //Get the followers Data
                } else {
                    //Show Error
                }
            }
            xhr1.send();
        } else {
            callback2(xhr.status);
        }
    }
    xhr.send();
}

//Fetch the User's Github details based on the user ID
fetchData(userId);
Enter fullscreen mode Exit fullscreen mode

The code gets harder to read and maintain when the callback functions are invoked in a nested order and that's what usually referred to as callback hell.

Callbacks are considered hard for a fact that the human mind perceives things sequentially or in a linear fashion, whereas, the way callback works in an inverted manner. This brings us to the next topic, inversion of control.

Inversion of Control

When your main function invokes the callback function, it basically hands over the program execution to the callback function. In essence, the entire program's flow depends on the response of the callback function, and then it proceeds from there onward. This nature of program execution is referred to as inversion of control.

Let us take a simple example and look at what I mean:

//Displays the name on the console
function sayHello(name) {
    //Displays the name on the console
    console.log(`Hello ${name}`);
}

//Function accepting the callback and a string parameter
function greeting(callback, name) {
    //The callback function passed here is 'SayHello'
    return callback(name);
}

//Call the greeting function
greeting(sayHello, 'Skay');
Enter fullscreen mode Exit fullscreen mode

Note: The execution of the function 'greeting' will be completed only after the execution of the function 'sayHello' (callback) completed. In essence, the flow of control is inverted.

Conclusion

A quick recap on what we've covered in this article:

  • What does asynchronous execution mean?
  • What are higher-order functions and callback functions?
  • How to create & pass callback functions?
  • Inversion of Control

I think we've covered in detail what Callback is all about and we should be good to move to the second part of the article on Promises and you can read them over here

Top comments (0)