How Does Node js Callback Work?

If you are learning node js or working as node js developer then you might have heard a word callback a lot. The callback concept can be confusing if you are just starting out with a node. But understanding it equally crucial to become a good node js developer and for cracking down any interview questions around it. In this article, we will try to understand what is callback functions and how does node js callback work in detail with some examples.

Before we go ahead directly start talking about the callbacks, first we need to understand what is a callback and why should you know it?

What is a callback function?

In asynchronous operation, the callback is a function that is called after completion of any task. Node js uses a single thread for operation. So, in that case, callbacks are very useful in the execution. Where main thread is not blocked for any reason and it can serve the other request as well.

A simple example of the callback will look like this

function GetData(callback)
{
    var data;
    function fecthData() {
       // Long task which is asynchornus 
        
       //if error occur
       if(err){
        var error= new Error("something went wrong...!") 
        callback(error);
       }
       data="Value";
       callback(data);
    }    
}

I know complete code block looking pretty complicated but is it very easy to understand. We have passed the callback function as a parameter to the “GetData” function, so whenever our result will be ready it will get passed via a callback function. The same thing happens in case of error as well.

In Synchronous operation, the main thread is waiting for the completion of a task. So during that time thread can not perform the other task which may hamper the application performance

See the below example from Node js

const fs=require('fs');
const result= fs.readFileSync('input.txt');
console.log(result.toString());

Node js come up with functions which can perform the synchronous and asynchronous task. Here we are using “readFileSync” (synchronous) function from “fs” (file system module).

The output from the input file:-

D:\node js callback>node SynchNodeExample.js
Hello World from input.txt ...!!

Here the input.txt file is small. So we quickly get the response from it. In case of the bigger file size, the execution of the code will be blocked at the result line until the file read completes.

Now we will use an async function with a callback that will execute the same task.

const fs=require('fs');
const result= fs.readFile('input.txt',function(err,data){
    if(!err)
    {
        console.log(data.toString());
    }
});
D:\node js callback>node SynchNodeExample.js
Hello World from input.txt ...!!

In the above code, we are using an async version of the reading a file that takes a function as callback and returns the response once the read operation is complete. In async function, code is executing in a non-blocking mode where the main thread can continue to execute the next operation without waiting for the response of any task.

We can write the same code with different syntax. Where we can pass the function as an argument and it will provide the same response as above.

const fs=require('fs');
function callbackReadFile(err,data){
    if(!err)
    {
        console.log(data.toString());
    }
}
const result= fs.readFile('input.txt',callbackReadFile);

callbackReadFile” is a callback function that is passing as a parameter to the “readFile” function.

What is a problem with callbacks?

Now you have understood what is a callback function and how it is work. So, you will say it looks good and easy so what’s the problem with callbacks?

The callback is one of the feature which node js heavily use but it can make your code ugly and completed if you use it extensively.

Let’s take a scenario where you have a chain of function need to be executed, and one function output is the input for the next function.

For this, we will take an example of an application where you have to load the application menu based on user roles. Here will do a series of a task in a callback function.

  1. Check the user credentials in the database.
  2. If the user is valid then check the assigned role.
  3. Fetch the menu list mapped to the specified role.

For the above example, we have a below code snippet


const userExists = function(username, password, callback){
    db.findUser(username, password, (error, userInfo) => {
        if (error) {
            callback(error)
        }else{
            db.getUserRole(username, (error, role) => {
                if (error){
                    callback(error)
                }else {
                    db.getMenu(role, (error, menu) => {
                        if (error){
                            callback(error);
                        }else{
                            callback(null, menu);
                        }
                    })
                }
            })
        }
    })
 };

In the above code, we are passing an anonymous function as a callback to the “findUser“, “getUserRole“, “getMenu” functions. We are passing the output of one function to another function to accomplish the task. As you can see the code is very much difficult to read and understand.

This kind of chaining of function is called as Callback Hell. Maintaining this kind of code is very difficult and for accommodating future changes you need to be very careful.

Hmm, So you must be thinking about what is a solution callback hell?

How to avoid callback hell?

Great…! Now you have understood what is the problem and why this occurs. Do we have any solution for callback hell?

Yes, Of course, we have. Below are the most popular solutions for the callback hell.

1. Breakdown small callback functions

What do I mean by this sentence?

We will create a separate callback function instead of passing it directly as an anonymous.


 function cbGetRoleMenu(err,role)
 {
    if(!err){
        db.getMenu(role, cbDisplayMenu);
    }
    console.log("This function will get the menus by user role")    
 }

 function cbGetUserRole(err,username)
 {
    if(!err)
    {
    db.getUserRole(username, cbGetRoleMenu);
    }
    console.log("This function will return user role")    
 }

 function FindUser(username, password)
 {
    if(!err)
    {
        db.findUser(username, password, cbGetUserRole);
    }
    console.log("This function will check if the user exists in database or not")    
 }

 FindUser('user1','test');

This approach is more clear and more maintainable as well. First, we will create the function and later we will use it. In this way, our code will not look messy and cluttery.

2. Promises

Promises help us to write more clearner code. Once the task is completed successfully then it will resolve successfully or in case of error, it will reject it. Node js support promises and it was heavily use before async and await feature.

Example of promises

var fs = require('fs');
var fileName = 'input.txt';
fs.readFileAsync(fileName ).then(function(txt) {
	Console.log("File read sucessfully" + txt);
}).catch(function(err) {
	console.log(err);
});

3. Async/Await

Async and await is synthetical sugar around the promises. It helps us to write the async code in a synchronous way. Which makes it more readable and clear to understand the code block.

Example of Async/Await

async function findUser(userId) {
    if (userId) {
        return await db.user.findById(userId);
    } else {
        throw 'User not found!';
    }
}

try {
	let user = await findUser(47745);
} catch(err) {
	console.error(err);
}

Conclusion

An understanding callback is a bit difficult when you are starting out with node js. I have tried to break down the concept and issues around it. I hope you have liked it. If you have any suggestions then kindly provide your comments and feedback.

Leave a Comment