Callback? What's that?

If you are new to Node.js, some of the things can be a bit confusing and overwhelming in the beginning. You might have several questions like:

  • What is the V8 engine?
  • What is the Event Loop?
  • How does asynchronous code run in Node.js?
  • What's a callback?
  • What are Promises?
  • What is Async/Await?

In this tutorial, we will address one of these questions:

What's a callback?

Callbacks

What are callbacks?

They are just functions.

Why do we call them callbacks then?

Well, its just a terminology we Javasript developers use for functions which are used in some special cases, usually involving asynchronous code execution. Usually in Node, we pass a function as an argument to an asynchronous function and we need this function (the one being passed) to execute inside the function to which we are passing it once the asynchronous operation completes. We call such functions (ones being passed as arguments) as callbacks. Makes sense?

Ok I take that as a "NO". Let me take a step back. Like many of the high level languages, functions are first class objects in Javascript.

What does that mean?

It means that you can treat functions same way as you would treat an object, a number or a string. You can assign a function as a value to a variable. Example:

var aNumber = 1;
var aString = 'Just a string';
var aNotSoUsefulFunction = function () {
    console.log('Hello, World!');
}

See? You can assign a function as a value to a variable, just like you can assing a number or a string as a value to a variable.

Hmm. Ok. I understand that. But what does it have to do with callbacks?

Wait. We'll get there shortly. First I need to make sure you understand the concept that functions are first class objects in Javascript. Another cool thing that we can do with functions is that we can pass them as arguments to another function. Same as you can pass a number or a string as an argument to a function. I'll show that to you shortly. First take a look at the function below:

// This function takes a string as an 
// argument and prints that to console
function printGreeting(name) {
    console.log('Hello', name);
}

printGreeting('Jerry');
printGreeting('Newman!');

In the above code snippet, we are passing a string as the argument name to the function printGreeting and that function is just printing the greeting to the console. Our function works nicely as long as the argument name is a string. Now, Javascript is a dynamically typed language. Which means just by looking at our function definition, we cannot tell that name argument is going to be a string or something else. The intended behaviour of our function is such that it expects the name to be a string, but there is no such type checking inside the function. We can pass whatever type we want. Although our function will not work as we want it to work. For example, we can pass a number as name. Example:

printGreeting(123);

Go ahead, try that. It doesn't fail. It will just print Hello, 123 as the output. What do you think will happen if we pass another function as the name argument. Here is what I am talking about:

function emptyFunction () {
    // this function doesn't do anything
}

function printGreeting(name) {
    console.log('Hello', name);
}

printGreeting(emptyFunction);

Did you try that? No? Please do and see what happens. Our printGreeting function won't fail even in this case. It will just print the function definition of our emptyFunction in this case. The point I am trying to highlight here is that functions can be passed as arguments to other functions in Javascript. In our code snippet above, we first defined the emptyFunction and then passed it as an argument to the printGreeting function. But we could also just define the emptyFunction on the fly while passing it as an argument. The above code can be rewritten as:

function printGreeting(name) {
    console.log('Hello', name);
}

printGreeting(function emptyFunction() {
    // this function doesn't do anything
});

In fact, we do not even need to name our emptyFunction here. So the code can be again rewritten as:

function printGreeting(name) {
    console.log('Hello', name);
}

printGreeting(function () {
    // this function doesn't do anything
});

Such functions are called anonymous functions in Javascript and you will see them everywhere in any Javascript code. It's very important that you understand the example above. If you didn't then please read it again until you do.

Our example here is kinda useless but we can do a lot of cool stuff by passing functions as arguments to other functions. We will gradually come to the useful functions but for now, here is another not so useful function:

function runAfterCountingToTen(functionToExecute, argumentToPass) {
    for (let i=1; i<=10; i++) {
        console.log(i);
    }
    functionToExecute(argumentToPass);
}

function printGreeting(name) {
    console.log('Hello', name);
}

runAfterCountingToTen(printGreeting, 'Jerry');

function printSquareOfNumber(n) {
    console.log(n*n);
}

runAfterCountingToTen(printSquareOfNumber, 7);

Here we have defined a generic function runAfterCountingToTen which expects two arguments:

  • A function to execute (example: printGreeting)
  • An argument that needs to be passed to that function (example: Jerry)

Our function will first print the numbers from 1 to 10 and then call the function passed in the first argument with the argument which is passed as the second argument. That is, in first case it will call:

printGreeting('Jerry');

And in the second case, it will call:

printSquareOfNumber(7);

What we are doing here is that we are passing a function functionToExecute (example printGreeting) to another function runAfterCountingToTen and we are executing our functionToExecute inside the runAfterCountingToTen.

The functionToExecute here represents a callback function. So, our printGreeting and printSquareOfNumber are both callback functions. Now let us revisit the definition that seemed confusing at the beginning of our tutorial:

Usually in Node, we pass a function as an argument to an asynchronous function and we need this function (the one being passed) to execute inside the function to which we are passing it once the asynchronous operation completes. We call such functions (ones being passed as arguments) as callbacks. Makes sense?

Does that make sense now?

Well, kinda. What's an asynchronous function?

Very good question. You see, here in our example, runAfterCountingToTen is not an asynchronous function. That's why I said "usually". Usually we use callbacks in Node.js when there is an asynchronous operation. But just the presence of a callback does not mean the function is asynchronous. Our runAfterCountingToTen is a synchronous function and it expects a callback as its first argument.

But what is an asynchronous function? And how does it differ from a synchronous function?

I understand your curiosity and we will definitely visit that topic in a different tutorial. But for now let the concept of callbacks sink in. Let me give you an assignment. Make sure that you first try to solve it on your own before scrolling donw to see the solution. So, here is the assignment:

Create a file called myFile.txt and enter some random content to it. Write a function which expects a filepath as its argument and prints the size of the file in bytes. Then pass this function and file myFile.txt as arguments to our function runAfterCountingToTen to see if it gives the desired output.

Hint: You need to use the fs.statSync function of Node.js

Solution:

.
.
.
.
.
.
.
.
.
.
.

const fs = require('fs');

function runAfterCountingToTen(functionToExecute, argumentToPass) {
    for (let i=1; i<=10; i++) {
        console.log(i);
    }
    functionToExecute(argumentToPass);
}

function printFileSize(filePath) {
    const fileSizeInBytes = fs.statSync(filePath).size;
    console.log('File Size in Bytes:', fileSizeInBytes);
}

runAfterCountingToTen(printFileSize, 'myFile.txt');

That was easy. I hope you were able to solve that?

Now, let us do something a little bit more realistic. Here is your second assignment:

Write a function called divide which expects three arguments: operand1, operand2 and a callback. Here is how the function signature should look like:

function divide(operand1, operand2, callback) {
    
}

callback is a function which expects two arguments. first argument is the error and second argument is result. Here is what its function signature should look like:

function myCallback(error, result) {

}

Inside the divide function, we need to check if operand2 is zero or not. If not, then execute the callback with first argument(error) as null and second argument as the result of the division of operand1 by operand2. If the operand2 is zero, then you need to execute the callback function with first argument as an error 'Error: Division by zero' and the second argument as null. Inside the myCallback function, check if error is not null. It it is not null, print the message 'An error occured during division:' then followed by the error message and then return. Else if error is null then print the result.

Solution:

.
.
.
.
.
.
.
.
.
.

function myCallback(error, result) {
    if (error) {
        console.log('An error occured during division:', error.message);
        return;
    }
    console.log(result);
}

function divide(operand1, operand2, callback) {
    if (operand2 !== 0) {
        const result = operand1 / operand2;
        callback(null, result);
    } else {
        callback(new Error('Error:  Division by zero'));
    }
}

divide(10, 2, myCallback);
divide(10, 0, myCallback);

You can also define the myCallback function as an anonymous function while calling the divide function. In that case the solution becomes like this:


function divide(operand1, operand2, callback) {
    if (operand2 !== 0) {
        const result = operand1 / operand2;
        callback(null, result);
    } else {
        callback(new Error('Error:  Division by zero'));
    }
}

divide(10, 2, function (error, result){
    if (error) {
        console.log('An error occured during division:', error.message);
        return;
    }
    console.log(result);
});

divide(10, 0, function (error, result){
    if (error) {
        console.log('An error occured during division:', error.message);
        return;
    }
    console.log(result);
});

Although we are repeating code here which is not a good thing. But I just wanted to share this style of passing the callback as an anonymous function because it is very common style and you will see it quite often while looking at any Node.js code snippet.

Please also remember that our divide function is still a synchronous function even though its function signature looks like that of an asynchronous function. I'll get to asynchronous functions in a different tutorial but first I just wanted to make sure you understand the concept of callbacks and get comfortable with that. Many beginners often find callbacks confusing and also sometimes assume that if a function accepts callback as an argument then it will be an asynchronous function. That's not true though. Callbacks do not imply asynchronous code and should not be confused with that.

I hope this tutorial solidifies your understanding of callbacks in Javascript. Happy coding :-)

Show Comments