Asynchronous JavaScript

srinivas moresrinivas more
10 min read

JavaScript is a single threaded programming language, it means only one operation can run at a time on the main thread. While this is true, JavaScript can behave asynchronous and parallel behavior through EventLoop,web APIโ€™s and micro/macro task queues.

While dealing with Asynchronous code you might often want to mimic output in a Synchronous way.

This can be acheived using callbacks and Promises.

Before even understanding how to use a callback lets understand what is a callback?

What is a callback?

Callback simply means that you are passing a function as a argument to another function. These callbacks can be called inside the main function to acheive various operations on args or even simplifying logic.

For example you might want to perform different arithmetic operations using callbacks.


javascriptCopy codefunction main(a, b, operation) {
    const res = operation(a, b); // Invokes the 'operation' function with a and b
    console.log(res);            // Logs the result of the operation
}

// Invoking 'main' with numbers and a function as arguments
main(3, 4, function add(a, b) {
    return a + b;  // Adds a and b, returning the result
});
let pizza;

function orderPizza() {
    console.log('Order pizza');

    setTimeout(() => {
        pizza = '๐Ÿ•';
        console.log('Pizza was ordered');
    }, 2000);
}

orderPizza();
console.log(`Eat ${pizza}`);

output:-
As you can see, I have simply passed an operation function onto my main callback which further applies some logic on the given args. In this way callbacks can be used.Now that you have understood about callbacks lets see how can we leverage callbacks to acheive synchronus behaviour.

When you are using callbacks to acheive Synchronous behaviour, you want to ensure the current task gets completed even before moving into next task.

let pizza;

function orderPizza() { console.log('Order pizza');

setTimeout(() => { pizza = '๐Ÿ•'; console.log('Pizza was ordered'); }, 2000); }

orderPizza(); console.log(Eat ${pizza});

Consider this example

l
As you can see from the above code we are getting Eat undefined, as we are trying to access a variable which gets intialised after 2 seconds or 2000 milliseconds, while console.log(`Eat {pizza}`) executes immediately.

What you can do is to isolate things properly and make it in sequence so you can do something like this below.

function orderPizza(cb){
    setTimeout(()=>{
        const pizza='๐Ÿ•';
        cb(pizza);
},2000);

function pizzaReady(pizza){
    console.log(`Eat the ${pizza}`);
}
orderPizza(pizzaReady);
call('Qoqi');

Output:-

As you can see I have passed a callback pizzaReady to the orderPizza function as a callback which executes inside setTimeout after 2 seconds. In this way pizza gets properly intialized and printed.

After 2 seconds

Here we are just trying to do one thing. But what if there are so many tasks you want to acheive synchronously when using asynchronous operation. Imagine I have multiple things that I gotta do , while waiting for the previous one to complete.

Have a look at this example, if I want to execute this in a synchronous fashion assuming there is some asynchronous function like setTimeout ,setInterval inside thing 1,2, my execution of callbacks would look something like this.

we can preserve the execution in order by invoking code something like above. Now still this is only managing three or four tasks what if there are more than 10, how would the code look like.

const fs = require("fs");

const path = require("path");

function fileOperations() {
  fs.readFile("./lipsum.txt", "utf-8", (err, data) => {
    if (err) {
      console.error("Erro reading file", err);
      return;
    }

    fs.writeFile("uppercase.txt", data.toUpperCase(), (err) => {
      if (err) {
        console.error("Error writing to file", err);
        return;
      }
      console.log("File uppercase.txt created and wrote succesfully");

      fs.writeFile("filenames.txt", "uppercase.txt\n", (err) => {
        if (err) {
          console.error("Error writing to file", err);
          return;
        }
        console.log("File filenames.txt created succesfullly");
        fs.readFile("uppercase.txt", "utf-8", (err, upperdata) => {
          if (err) {
            console.error("Error while");
            return;
          }
          const sentences = upperdata.toLowerCase().split(/[|,?\n.]+\s*/);

          fs.writeFile("splittedData.txt", "", (err) => {
            if (err) {
              console.error("Error while creating the file", err);
              return;
            }
            console.log("File splittedData.txt created");
            for (let sentence of sentences) {
              fs.appendFile("splittedData.txt", sentence + "\n", (err) => {
                if (err) {
                  console.error("Erroe while appending data");
                  return;
                }
              });
            }

            fs.appendFile("filenames.txt", "splittedData.txt" + "\n", (err) => {
              if (err) {
                console.error("Error while appending data");
                return;
              }
              const sortedData = upperdata.split(" ").slice().sort().join(" ");
              fs.writeFile("sortedData.txt", sortedData, (err) => {
                if (err) {
                  console.error("Error while eriting to sorted Data", err);
                  return;
                }
                console.log("File sortedData.txt created succesfully");
                fs.appendFile(
                  "./filenames.txt",
                  "sortedData.txt" + "\n",
                  (err) => {
                    if (err) {
                      console.error("Error while writing to sortedData", err);
                      return;
                    }
                    const filenamesPath = path.join(__dirname, "filenames.txt");
                    fs.readFile(filenamesPath, "utf-8", (err, data) => {
                      if (err) {
                        console.error(
                          "Error while reading filenames:",
                          err.message
                        );
                        return;
                      }

                      // Split data by newline and filter out empty lines
                      const files = data
                        .split("\n")
                        .filter((file) => file.trim().length > 0);
                      console.log(files);
                      // Delete each file asynchronously
                      files.forEach((file) => {
                        const deleteFile = path.join(__dirname, file);

                        fs.unlink(deleteFile, (err) => {
                          if (err) {
                            console.error(
                              `Error while deleting ${deleteFile}:`,
                              err.message
                            );
                            return;
                          }
                          console.log(
                            `File ${deleteFile} deleted successfully`
                          );
                        });
                      });
                    });
                  }
                );
              });
            });
          });
        });
      });
    });
  });

  // fs.readFile(filenamesPath, 'utf-8', (err, data) => {
  //   if (err) {
  //     console.error('Error while reading filenames:', err.message);
  //     return;
  //   }

  //   // Split data by newline and filter out empty lines
  //   const files = data.split('\n').filter(file => file.trim().length > 0);
  //   console.log(files);
  //   // Delete each file asynchronously
  //   files.forEach((file) => {
  //     const deleteFile = path.join(__dirname, file);

  //     fs.unlink(deleteFile, (err) => {
  //       if (err) {
  //         console.error(`Error while deleting ${deleteFile}:`, err.message);
  //         return;
  //       }
  //       console.log(`File ${deleteFile} deleted successfully`);
  //     });
  //   });
  // });
}

module.exports = {
  fileOperations,
};

That would look something like this.

This is phemomenon is called callback hell.

What is callback Hell?

Callback Hell in JavaScript

Callback hell refers to a situation in JavaScript where multiple asynchronous operations are nested inside each other using callback functions, creating code that becomes difficult to read, maintain, and debug. This issue occurs when many asynchronous tasks need to be performed sequentially, with each task depending on the result of the previous one.

This will really mess up code readability and could be an instant turn off for anyone who are seeing your code. For this reason we use Promise chaining.

Getting to know Promises

What is a promise?

Promise in JavaScript is an object that represents the eventual completion or failure of an asynchronous operation. It is used for handling asynchronous operations, such as making API calls or reading files, in a more organized and readable way.

States of promises

Promises can be in different states depending upon which is executed either resolve or reject.

  • Pending: The initial state, neither fulfilled nor rejected.

  • Fulfilled: The operation was successful, and the promise has a value.

  • Rejected: The operation failed, and the promise contains a reason for the failure (an error).

  •   function promiseOutput(val){
          return new Promise((resolve,reject)=>{
              if(val>10){
                  return reject('Vlaue should be less than 10');
              }
              return resolve('value is processed');
          })
      }
    
      const p1 =promiseOutput(2);
      p1.then((data)=>{
          console.log(data);
      })
      .catch((err)=>{
          console.log('give different value');
      })
    

From the above code we can see depending upon value if its greater than 10, promise rejects which normally gives an reject error but because of catch function the error is being caught, and when its resolved we can see then block getting executed.

for value of 2 we would get output something like this

lets intentionally reject a promise and see what we will get.

As expected we can see catch block in action, But what if there is no catch block.

As expected we can see an promise rejection error which is not handled by any ways.

Then block and catch block

Using .then(),finally() and .catch() Blocks

  1. .then():
    Used to handle the fulfilled state. It takes a function that receives the resolved value as a parameter.

     javascriptCopy codelet promise = new Promise((resolve, reject) => {
       setTimeout(() => resolve("Data fetched successfully!"), 1000);
     });
    
     promise.then((result) => {
       console.log(result);  // Output: Data fetched successfully!
     });
    
  2. .catch():
    Used to handle the rejected state. It catches any errors that occur in the promise chain.

     javascriptCopy codelet promise = new Promise((resolve, reject) => {
       setTimeout(() => reject("Error occurred!"), 1000);
     });
    
     promise
       .then((result) => {
         console.log(result);  // This won't execute as the promise is rejected
       })
       .catch((error) => {
         console.error(error);  // Output: Error occurred!
       });
    
  3. Chaining .then() and .catch():
    You can chain multiple .then() calls to perform sequential tasks, and use .catch() to handle any errors along the way.

     javascriptCopy codelet promise = new Promise((resolve, reject) => {
       resolve(10);
     });
    
     promise
       .then((value) => value * 2)  // 10 * 2 = 20
       .then((value) => value + 5)  // 20 + 5 = 25
       .then((value) => {
         console.log(value);  // Output: 25
       })
       .catch((error) => {
         console.error("Error:", error);
       });
    

This structure helps in managing asynchronous code more cleanly, avoiding callback hell and making error handling easier.

Promises to behave synchronously in a asynchronous environment.

How can we use promises to mimic synchronous behaviour using asynchronous functions.

function givePromise(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve("Promise is being resolved after 2 seconds");
        },2000);
    })
}

givePromise()
.then((data)=>{
    console.log(data);
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve('This promise will execute after 3 seconds');
        },3000);
    })
})
.then((data)=>{
    console.log(data);
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve('This promise will execute after 4 seconds');
        },5000);
    })
})
.then((data)=>{
    console.log(data);
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve('This promise will execute after 5 seconds');
        },5000);
    })
})
.then((data)=>{
    console.log(data)
})
.catch((err)=>{
    console.log(err);
})

This code demonstrates promise chaining where multiple promises are executed in sequence. Each .then() block returns a new promise that triggers the next step in the chain after a delay. The chaining ensures that each asynchronous operation waits for the previous one to complete before proceeding.

Walkthrough of the code.

givePromise() Function

  • It returns a new promise that resolves after 2 seconds.

  • Once resolved, the first .then() block is executed.

javascriptCopy codefunction givePromise() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Promise is being resolved after 2 seconds");
    }, 2000);
  });
}
  • First Promise Execution

    • After 2 seconds, the message "Promise is being resolved after 2 seconds" is logged.

    • The .then() block returns another promise, which will resolve after 3 seconds.

    javascriptCopy codegivePromise()
      .then((data) => {
        console.log(data);  // Output: Promise is being resolved after 2 seconds
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve('This promise will execute after 3 seconds');
          }, 3000);
        });
      })
  • Second Promise Execution

    • After 3 more seconds, the message "This promise will execute after 3 seconds" is logged.

    • It returns another promise that resolves after 5 seconds.

    javascriptCopy code  .then((data) => {
        console.log(data);  // Output: This promise will execute after 3 seconds
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve('This promise will execute after 4 seconds');
          }, 5000);
        });
      })
  • Third Promise Execution

    • After 5 seconds, the message "This promise will execute after 4 seconds" is logged.

    • It returns a new promise that resolves after 5 more seconds.

    javascriptCopy code  .then((data) => {
        console.log(data);  // Output: This promise will execute after 4 seconds
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve('This promise will execute after 5 seconds');
          }, 5000);
        });
      })
  • Fourth Promise Execution

    • After another 5 seconds, the final message "This promise will execute after 5 seconds" is logged.
    javascriptCopy code  .then((data) => {
        console.log(data);  // Output: This promise will execute after 5 seconds
      })
  • Error Handling with .catch()

    • If any promise in the chain is rejected, the .catch() block will execute to log the error.
    javascriptCopy code  .catch((err) => {
        console.log(err);
      });

Summary

callbacks vs Promises

1.Callbacks

  • Definition: A callback is a function passed as an argument to another function, executed after an asynchronous operation completes.

  • Problem: Callback Hell occurs when multiple nested callbacks are used for sequential tasks, creating hard-to-read, error-prone code (the "pyramid of doom").

2.promises

  • Definition:A promise is represents the eventual result of an asynchronous operaation in one of three states:pending,resolved or rejected.

  • How promises Solve callback Hell:

  • Promises flatten nested callbacks by chaining .then() blocks.

  • They alos make error handling easier with a .catch() block.

Conclusion

Why Avoid Callbacks for Complex Asynchronous Code

  • Callbacks are fine for simple tasks but become unmanageable when you have multiple dependent operations.

  • Promises offer a more structured way to handle asynchronous tasks, reducing nested callbacks.

  • Async/Await is the cleanest solution as it makes the code readable and maintainable like synchronous code.

In modern JavaScript, async/await is preferred over callbacks and promises for handling asynchronous operations to avoid callback hell while keeping code readable and easy to debug.

0
Subscribe to my newsletter

Read articles from srinivas more directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

srinivas more
srinivas more