Asynchronous Javascript

How JS executes the code?

JavaScript is a single-threaded programming language. It can perform one task at a time. It will block other tasks until the current task completes its execution Every browser has its own JavaScript Engine.

Whenever the JavaScript Engine scans a script file. It makes an environment called execution context. So whenever we run a JavaScript file a Global execution context is created. It contains two blocks one block is for memory and another block is for code.

  • There are two phases

    Memory creation phase

  • In this phase, the javaScript Engine allocates memory for variables and functions
  • Initially variables get a value of undefined and if we have functions in the code the entire code of the function will be stored in the memory phase.

    Code Execution Phase

  • In this phase, the javaScript Engine executes the code in the execution context.

  • It will give the variable original value instead of undefined after execution.

For the functions inside the code, it will create a new execution context inside the global context it also contains two blocks memory and code. It has also two phases creation and execution phase same as the global execution context.

  • let's have a look at a simple program to clearly understand this concept
var n=2;
function square(num){
  var result = num*num;
  return result;
}
var square2 = square(n);
var square3 = square(4);
  • Now once we run the code the global execution context is created and in the memory phase the variable n is stored as undefined and after execution the value of n is changed to 2.
  • For the function inside the code new execution context is created inside the global context.
  • Now the square2 function will get executed and after execution, the execution context gets removed
  • Now the square3 function will get executed and after execution, the execution context gets removed.
  • Once the code completes the execution the global execution context also gets removed.

Call Stack

call stack plays an important role in keeping track of all the contexts It follows the LIFO principle(Last- In- First -Out). When the engine first starts executing the script, it creates a global context and pushes it on the stack. Whenever a function is invoked, similarly, the JS engine creates a function stack context for the function pushes it to the top of the call stack, and starts executing it.

When execution of the current function is complete, then the JavaScript engine will automatically remove the context from the call stack and it goes back to its parent.

Difference between Synchronous and Asynchronous JavaScript

Synchronous JavaScript:

It is a single-threaded. It can perform one task at a time. It will block other tasks until the current task completes its execution. Synchronous operations are easier to debug because they perform the operations one by one.

Asynchronous JavaScript

It is multi-threaded. It can perform multiple tasks at a time. It doesn't block other tasks while executing current tasks. Asynchronous operations are essential for tasks that involve waiting for external resources or events, such as fetching data from a server, reading from a file, or handling user input in a non-blocking manner.

How can we make sync code into async?

To make our Synchronous code work like Asynchronous code we have mechanisms like callback functions and promises.

  • let's have a look at a simple program to clearly understand this concept
    function fetchData(callback) {
    setTimeout(() => {
      callback(data);
    }, 2000);
    }
    console.log("start");
    fetchData((result) => {
    console.log(result);
    });
    console.log("end");
    
  • Here What happens is first It will print the two console statements in output then after two seconds we will get the data. This way we can make synchronous code work like Asynchronous.

What are callbacks & what are the drawbacks of using callbacks

Callbacks:

It means passing a function as an argument inside another function. It will get executed once the specific tasks are completed.

function displayName(name, callback) {
  console.log(name);
  callback();
}
function getName() {
  console.log("kanha");
}
displayName("radha", getName);
`
  • We have two drawbacks while using callbacks
  • callback hell
  • Inversion of control
    1. callback hell: It means multiple callback functions nested inside others
    2. Inversion of control: It means we are giving control of one function to another so it doesn't guarantee the execution of another function once the first one completes its operation. we simply give control of one function to another function.
  • Example of callback Hell
  createOrder(args, function() {
     proceedToPayment(args,function(){
         getPaymentDeatils(args,function(){
             getReciept(args,function(){
                 getUserInfo(args,function(){
                })
            })
        })
    })
  });
`

How do promises solve the problem of inversion of control?

To solve the problem of Inversion of control and callback hell we have a mechanism called promises.

Promise:

a promise is an Object that represents the eventual completion of an asynchronous operation. It means it is promising that it will complete the task in the future. Promise has three states

Pending:

Initially, the promise will go to a pending state once after its creation.

Fulfilled:

If the promise gets settled it will go to the fulfilled state.

Rejected:

If the promise gets rejected it will go to the rejected state.

  • Here is an example to solve the problem of inversion of control
function createOrder(args) {
  return new Promise((resolve, reject) => {
    console.log("Creating order...");
    setTimeout(() => {
      resolve(args);
    }, 1000);
  });
}

function proceedToPayment(args) {
  return new Promise((resolve, reject) => {
    console.log("Proceeding to payment...");
    setTimeout(() => {
      resolve(args);
    }, 1000);
  });
}

function getPaymentDetails(args) {
  return new Promise((resolve, reject) => {
    console.log("Getting payment details...");
    setTimeout(() => {
      resolve(args);
    }, 1000);
  });
}

function getReceipt(args) {
  return new Promise((resolve, reject) => {
    console.log("Getting receipt...");
    setTimeout(() => {
      resolve(args);
    }, 1000);
  });
}

console.log("Start");
createOrder(args)
  .then(proceedToPayment)
  .then(getPaymentDetails)
  .then(getReceipt)
  .then(() => {
    console.log("Order creation completed");
    console.log("End");
  })
  .catch((error) => {
    console.error("An error occurred:", error);
  });

What is an event loop?

In our code, if we have callback functions are promise to fetch data or to do some action it will take some time but our call stack will remove the global execution context once the code completes execution. But to fetch data or to complete the callback function it needs some time. So here "event loop" comes into the picture The task of the event loop is to check if the call stack is Empty or not. If the call stack is Empty then it will check the callback queue if the callback queue has tasks then it will send the task to the call task then it will get printed on the console.

What are the different functions of promises?

Promise.all():

It takes an array of promises and executes all parallel. If all promises resolve it will return an array of all promises. If any one of the promises is rejected it will reject all promises.

 const p1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
         resolve('p1 success')
     },3000);
  })

 const p2 = new Promise((resolve,reject)=>{
     setTimeout(()=>{
         resolve('p2 Sucess');
     },3000);
  })

 const p3 = new Promise((resolve,reject)=>{
     setTimeout(()=>{
         resolve('p3 success')
     },2000);
  })

 Promise.all([p1,p2,p3])
 .then((result)=>{
     console.log(result)
 })
 .catch(error=>{
     console.error(error);
 })
  • Here we will get the output like [ 'p1 success', 'p2 sucess', 'p3 success' ]

promise.allSettled():

It takes an array of promises and returns the results of all promises whether it is rejected or resolved it will wait for all the promises to settle.

const p1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve('p1 success')
    },3000);
})

const p2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve('p2 success');
    },3000);
})

const p3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        reject('p3 Fail')
    },2000);
})

Promise.allSettled([p1,p2,p3])
.then((result)=>{
    console.log(result)
})
.catch(error=>{
    console.error(error);
})
  • Here it will output like
    [
    { status: 'fulfilled', value: 'p1 success' },
    { status: 'fulfilled', value: 'p2 success' },
    { status: 'rejected', reason: 'p3 Fail' }
    ]
    

promise.any():

It takes an array of promises and waits for any promise to settle then it will return the result of a resolved promise if all promises are rejected then it will give an aggregate error.


const p1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        reject('p1 Fail')
    },1000);
});

const p2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        reject('p2 Fail');
    },2000)
})

const p3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve('p3 sucess')
    },3000)
});

Promise.any([p1,p2,p3])
.then((result)=>{
    console.log(result);
})
.catch((error)=>{
    console.error(error);
})
  • Here we will get the output like p3 success

promise.race()

It takes an array of promises and returns the result of the first settled promise whether it is rejected or resolved.

const p1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve('p1 success')
    },3000);
})

const p2 = new Promise((resolve,reject)=>{
     setTimeout(()=>{
         reject('p2 Fail');
    },3000);
})

const p3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve('p4 success')
    },5000);
})

Promise.race([p1,p2,p3])
.then((result)=>{
    console.log(result)
})
.catch(error=>{
    console.error(error);
})
  • Here we will get the output like p1 success