The callback is an asynchronous equivalent for a function. It is called following every task. In Node.js, callbacks are frequently used. All APIs of Node are drafted in a way that supports callbacks.  To illustrate, when a function commences reading a file, it instantly returns the control to the execution environment to execute the succeeding instruction.

In Node.js, once file I/O is concluded, it would call the callback function. There is no hindrance or waiting for File I/O. This presents Node.js as highly scalable, as it can process an extraordinary number of requests without pausing for any function to return results.

Understanding the callback pattern for asynchronous programming

In the asynchronous programming model, something happens one at a time. An asynchronous model acknowledges multiple things to occur at the same time. When you commence an action, your program proceeds to run. When the step completes, the program is acquainted and gets access to the result (for example, the data read from disk).

Node.js encourages an asynchronous coding method from the ground up, in contrast to several popular web frameworks. There are numerous vital things to be conscious of when learning to write asynchronous code – and at the same time, you will frequently find your code executing in highly unexpected ways.

Various functions in Node.js core have both synchronous and asynchronous versions. It will be far better to use asynchronous operations in most conditions; otherwise, why use Node.js?

Asynchronous continuation-passing technique

let’s consider an example where a function is asynchronous, which is as follows:

function exampleAsync(a, b, callback) {
  setTimeout(function() {
    callback(a + b);
  }, 100);
}
console.log('Before asynchronous call’);
exampleAsync(2, 3, function(finalresult)  
{
    console.log('Result: ' + finalresult);
}); console.log('After asynchronous call');

OutPut:

Before asynchronous call 
After asynchronous call 
Result: 5

setTimeout() triggers an asynchronous process; it will not wait for the callback to get executed. It returns quickly, providing the control back to exampleAsync(), and then back to its caller.

The following image shows how this works:

Asynchronous continuation-passing technique

Examples of the callback pattern in Node.js

A callback is a function called when the task finishes, and a callback function allows other code to run in the meantime. Using the Callback concept, Node.js can process many requests without waiting for any function to return the result, making Node.js highly scalable. For example: In Node.js, when a function starts reading the file, it returns the control to the execution environment immediately to execute the next instruction. Once file I/O gets completed, the callback function will get called to avoid blocking or wait for File I/O.

Example 1: Reading a file synchronously in Node.js. Create a text file synch.txt with the following content:

Hello, this is my first testing of synchronous content.

Create a first.js file:

var fs = require("fs"); 
var datatxt = fs.readFileSync('synch.txt'); 
console.log(datatxt.toString()); 
console.log("Execution ends");

Output:

Hello, this is my first testing of synchronous content.
Execution ends

Information: The fs library is loaded to handle file-system associated operations. The readFileSync() function is synchronous and blocks execution till terminated. The function blocks the program until it reads the file, and then only it proceeds to end the program.

Example 2: Reading a file asynchronously in Node.js. Create a text file asynch.txt with the content as

Hello, this is my first testing of asynchronous content.

var fs = require("fs");     
fs.readFile('asynch.txt', function (ferr, dataasynch) {  
    if (ferr) return console.error(ferr);  
    console.log(dataasynch.toString());  
});  
console.log("Execution ends"); 

Output:

Execution ends
Hello, this is my first testing of asynchronous content.

Information: The fs library is loaded to handle file-system-related operations. The readFile() function is asynchronous, and the control returns immediately to the next instruction in the program while the function keeps running in the background. A callback function is relinquished, which gets called when the task running in the background is finished.

Getting trapped in callback hell

Callback Hell is an anti-pattern detected in the code of asynchronous programming. It is a slang phrase applied to define a cumbersome number of nested “if” statements or functions. If the application logic gets complex, a few callbacks appear harmless. But once your project demands grow, it is common to fall under piling layers of nested callbacks.

The callback is a function where “A” is passed to another function, “B,” as a parameter. The function “B” then executes the code “A” at some point. The invocation of “A” can be immediate, as in a synchronous callback, or it can occur later as in an asynchronous callback.

var callbackhell = require(‘fs’)
callbackhell.readFile(‘test.json’, function(‘err’, results){
if(err){
console.log(err);
}
console.log(JSON.parse(results).name)
});

In the code, we call readFile and pass it as a second parameter function (Callback Hell). readFile will execute the callback bypassing the procedure’s results to parameters.

The use of callbacks makes the code tedious to write and manage. It increases the difficulty of identifying the application’s flow, which is an obstacle, hence the popular name of Callback Hell.

What’s more dangerous than callback hell?

Not fixing the nested callback hell

Using Promises to write asynchronous code

Promises are the function that a value would be returned at a later time. Instead of returning concrete values, these asynchronous functions return a Promise object, which will at some point either be fulfilled or not.

A promise represents an asynchronous operation. It means a process that has not been completed yet but is expected to in the future. Let’s have a look at a simple file read example without using promises:

fs.readFile(filePath, (err, result) => {
     if (err) { console.log(err); }
     console.log(data);
});
if the readFile function returned a promise, the logic would be  written as below
var fileReadandPromise = fs.readFile(filePath);
fileReadandPromise.then(console.log, console.error)

The fileReadandPromise is passed multiple times in a code where you need to read a file. This helps in writing robust unit tests for your code since you now only have to write a single test for a promise. And more readable code!

Promise.all()

The Promise. all() method accepts an iterable of promises as an input and returns a single Promise that fixes to an array of the input promises’ results.

const promise1 = Promise.resolve(5);
const promise2 = 54;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});

Promise.any()

Promise.any() takes an iterable of Promise objects and, as soon as one of the promises in the iterable fulfills, returns a single promise that resolves with the value from that promise. If there are no promises in the iterable fulfill, then the returned promise is rejected with an AggregateError, a new subclass of Error that groups together individual errors. This method is the opposite of Promise.all().

const promiseand1 = Promise.reject(0);
const promiseand2 = new Promise((resolve) => setTimeout(resolve, 100, 'Large'));
const promiseand3 = new Promise((resolve) => setTimeout(resolve, 500, 'Small'));
const promises = [promiseand1, promiseand2, promiseand3];
Promise.any(promises).then((value) => console.log(value));

Using Async / Await for handling asynchronous code

Initial versions of Node didn’t have the Node architecture single-threaded and asynchronous. The intricacy with this kind of code is that this kind of position can create many problems, and the code can get messy when there are several functions, and this situation is called callback hell.

Promises and function chaining were introduced to overcome this situation.

By Node v8, the async/await feature was finally wheeled out to deal with Promises and function chaining. The functions were not necessarily required to be chained after another; they simply await the function that returns the Promise. But the async function is needed to be declared before awaiting a function returning a Promise.

Examples of Async / Await

The code looks like the following.

async function testfun1(req, res){
  let resp1 = await request.get('http://localhost:8080');
    if (resp1.err) { console.log('error');}
    else { console.log('Response Fetched');
}

Explanation

The code above essentially demands the JavaScript engine driving the code to wait for the request.get() function to accomplish before moving on to the next line to execute it. The request.get() function returns a Promise for which the user will await. Before async/await, if it is required to check that the functions are running in the desired sequence, i.e. one after the another, chain them one after the another or register callbacks. 

Code review and understanding become comfortable with async/await, as observed from the above example.

Error handling in the case of async / await

Formulating exceptions
An exception is built using the throw keyword:
throw value
As soon as the above line executes, the standard program flow stops and the control is held back to the most imminent exception handler.

Typically, in client-side code, a value can be any JavaScript value, including a string, a number or an object.

In Node.js, we don’t throw strings; we throw Error objects.

Error objects
An error object is an object that is either an instance of the Error object or extends the Error class provided in the Error core module:

throw new Error('Out of Mall')
OR
class NoPersoninMallError extends Error {
  //...
}
throw new NoPersoninMallError()

Exception handling

An exception handler is a try/catch statement.
Any exception created in the lines of code in the try block is as below:

try { 
//code would be written here. 
} catch (e) {} 
//e is the exception value. 

Error handling with async/await

Using async/await, the errors can be caught as below:

async function AnyFunction() { 
  try { 
    await anotherFunction() 
  } catch (err) { 
    console.error(err.message) 
  } 
}

Conclusion

Node.js is more beneficial to the developers in association with its disadvantages. What’s more important is that it has extended the JavaScript applications area and can be used for both front-end and back-end servers.

Node.js is, without a doubt, one of the more exciting technologies in use today, and it has grown into one of the most popular platforms used for web applications, services, and desktop apps. With time, more and more business organizations have opted to use Node.js and are consistently getting positive results.