JavaScript Callbacks Explained

JavaScript Callbacks Explained

·

10 min read

Introduction

I think Async is one of the most powerful yet not-so-direct concepts to grasp at first for JavaScript Newbies.

In this article, I have covered the following topics:

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

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 the 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));

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

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 the '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 = (a, b) => {
    return a + b;
}

const higherOrderFunction = (callback) => (a, b) {
    return callback(a, b);
}

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.

So, 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);

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 the 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);

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');

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 great detail on what Callback is all about and an excellent article to continue reading is on Promises and you find it over here

Don't forget to subscribe and connect with me on Twitter @skaytech

You'll also enjoy the following articles:

Did you find this article valuable?

Support Skay by becoming a sponsor. Any amount is appreciated!