Skip to main content

Command Palette

Search for a command to run...

A(Synchronous) JavaScript

Learn every thing about Asynchronous JavaScript

Published
•10 min read•View as Markdown

Introduction

Before understanding how asynchronous JavaScript (JS) works, let's get to know how JS code is executed. The code execution has two phases,which are Memory Creation phase and Code Execution phase. In memory allocation phase, each variable including anonymous functions assigned as values to a variable are declared with value undefined, and functions are stored as the whole code block. And in second phase, the code execution begins by allotting the actual values to variables and executes functions, here the functions are stacked in Call Stack, where the latest entered code block will get executed first, these all happens in environment which is called Execution Context. But these all works when the code is synchronous, that is when one code executes at a time, but the most real world applications of JS are asynchronous in nature, So it is impossible for call stack alone to handle multi-threads. But there are other components works along with call stack which makes JS async, these components include Callback Queue and Microtask Queue and Event Loop .Later in the article let's know about all these components.

Synchronous v/s Asynchronous

Synchronous code is executed one line after other, that means the next line has to wait until the current line of code is executed, whereas asynchronous code does not block the execution, here parallel execution happens.

Example for Synchronous code

function one(){
two();
console.log("function 'one' out of call stack");
}   
function two(){
console.log("function 'two' out of call stack");
}
one();
/*
output
function 'two' out of call stack
function 'one' out of call stack
*/
// line by line execution

Example for asynchronous code

function asyncFunction(){
console.log("logs first");
setTimeout(()=>{
console.log("logs last");
},0);
console.log("logs second");
}
asyncFunction();

/* output
logs first
logs second
logs last
*/
// logs last console second,and parallely processes
// the console in setTImeout

The Event Loop

The Event Loop can be considered as an organiser, it's duty is to activate different components of execution environment of JS, the components are Callstack, callback queue and Microtask queue. The event loop activates Callstack with high priority, if Callstack is empty, it checks for Microtask queue and sends all the operations that are waiting to get executed, if Microtask is empty Callback Queue is activated.

I know, you were confused on what are Microtask queue and Callback queues, for time being just remember these key points regarding these queues.

  • Microtask Queue has high priority than Callback Queue.

  • Callback Queue gets ordinary callback functions, usually from APIs like setTimeout().

  • Microtask Queue gets the callback functions coming through Promises.

All about Callbacks

Callback is a function which is passed as an argument to another function. These callbacks can be implemented in two ways, let's explore them through examples.

Passing function name

function callbackTaker(callback){
    callback("callback invoked");
}
function callback(message){
    console.log(message);
}
callbackTaker(callback); 
// output - callback invoked

Passing direct function

function callbackTaker(callback){
    callback("callback invoked");
}
callbackTaker((message)=>{
    console.log(message)
}); 
// output - callback invoked

Callback Queue

Earlier we learned that there are three components in execution environment which are organised by event loop, Callback Queue is one of them which has least priority in execution order compared to callstack and microtask queue. Callback Queue receives the callbacks coming from non promise asynchronous operations like setTimeout and callback based fs (File System) methods.

Callback Hell

Callback Hell is a phenomenon when a callback function calls another function as a parameter and if this continues for some more steps forms a horizontally increasing code which makes code unreadable and hard to debug the code. Here's a little example for callback hell

function houseOfCallbacks() {
  setTimeout(() => {
    console.log("this is first callback, now go deeper !!!");
    setTimeout(() => {
      console.log("callback hell level 1");
      setTimeout(() => {
        console.log("it's simple to debug right?");
        setTimeout(() => {
          console.log("think if this continues for another 10 levels??");
          setTimeout(() => {
            console.log("what if these inner functions consist of complex operations instead of simple consoles ?");
            setTimeout(()=>{
                console.log("think if there is a bug in this hell, and your were assigned to debug it, hahaha!!");
                setTimeout(()=>{
                    console.log("ok ok let's stop this. This is final level of callback")
                },1000)
            },1000)
          },1000);
        }, 1000);
      }, 1000);
    }, 1000);
  }, 1000);
}
houseOfCallbacks();
// guess the output
// this is also called as Pyramid of Doom

Inversion of Control

This is another issue with callbacks. When you are passing a callback into a function (may be a third party API), we are not sure if the callback we passed was compatible with the code of that function or not, also there is no guarantee if that callback was ever called, or may be called twice. So passing callback to another function is directly passing the control of our code to that third party API or any function which cannot be modified. Let's see an example now,

function someThirdPartyAPI(callback){
    let count = 0;
    callback(++count); // we need callback to be called once
    callback(++count); // hey!! thirdPartyAPI why are you calling it again?
    callback(++count); // AGAIN ??
}
someThirdPartyAPI((data)=>{
    console.log(data);
});

Yes !, I know you what you were thinking. We have discussed a lot about callbacks, callback hell and inversion of control. But how can we overcome these issues with callbacks ? Here comes our hero of JavaScript....

Promises

The famous and most authentic documentation for JS is MDN which defined Promise as an object that represents an eventual completion or failure of an asynchronous operation. It maybe confusing to understand this definition but all it will makes sense in next few minutes. Firstly, let's see how a Promise is initialised and what are the states of promises.

Promise initialisation

function promFunction(foo) {
  return new Promise((resolve, reject) => {
    if (foo) {
      resolve("hey promise resolved");
    } else {
      reject("you rejected the promise brooo");
    }
  });
}

// resolving promise
promFunction(true)
  .then((value) => {
    console.log(value); // enters 'then' block and logs :  hey promise resolved
  })
  .catch((message) => {
    console.log(message);
  });

// rejecting promise
promFunction(false)
  .then((value) => {
    console.log(value);
  })
  .catch((reason) => {
    console.log(reason); // enters 'catch' block and logs : you rejected the promise brooo
  });

when promise is created, it has a default state of pending. Any promise which is pending should either get fulfilled (resolved) or rejected. promise which is either fulfilled or rejected is called settled promise. In the above example, if true is passed to function, the promise gets resolved and if false is passed it gets rejected.

The methods '.then' method is used to hold the value returned by resolve function in the promise, this can be any value or another promise or any function, also the '.catch' method is used to hold the reason sent by the reject function in the promise, this reason can be any value (not limited to a string) or another promise or function. The then and catch methods are pushed into Microtask Queue, cause these are callback functions based on promises. If one or more then blocks are attached to each other, then it is called promise chaining. Using promise we can overcome callback hell and inversion of control.

// Example for promise chaining
// shows how callback hell is overcomed
// for time being let's assume all promises are resolved
function houseOfPromises(input) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(input);
    }, 1000);
  });
}

houseOfPromises("this is first promise").then((value)=>{
    console.log(value);
    return houseOfPromises("this returned promise is attached to next promise");
})
.then((returnedPromise)=>{
    console.log(returnedPromise);
    return houseOfPromises("by attaching number of 'then' blocks is called promise chaining");
})
.then((thirdLevelPromise)=>{
    console.log(thirdLevelPromise);
    return houseOfPromises("this will not create an hell and will be easy to debug");
})
.then((easyDebug)=>{
    console.log(easyDebug);
    return houseOfPromises("code readablity is improved compared to callback hell");
})
.then((looksCool)=>{
    console.log(looksCool);
    return houseOfPromises("inversion of control issue is solved using promise, there is no need of passing callbacks to thirdPary APIs");
})
.then((lastReturn)=>{
    console.log(lastReturn);
    return "we can return a value as well"
})
.then((finalMessage)=>{
    console.log(finalMessage);
})
.catch((error)=>{
    console.log("any error in above chain will be catched here, this block can be written in the middle also");
    console.log(error.name,erorr.message);
})
/* output
this is first promise
this returned promise is attached to next promise
by attaching number of 'then' blocks is called promise chaining
this will not create an hell and will be easy to debug
code readablity is improved compared to callback hell
inversion of control issue is solved using promise, there is no need of passing callbacks to thirdPary APIss
we can return a value as well
*/

Promise.all()

all method in promises accepts a list of promises, and if all promises are resolved, it returns list of resolved values, if any one promise is rejected then returns the rejected reason for that promise. To see this practically, lets make some array combinations as shown in below code,

function promFunction(foo,num) {
    return new Promise((resolve, reject) => {
      if (foo) {
        resolve("hey promise resolved "+num);
      } else {
        reject("you rejected the promise "+num);
      }
    });
  }

// list of all resolved promises
let proms1 = [promFunction(true,1), promFunction(true,2), promFunction(true,3)];
// list of some resolved promises
let proms2 = [promFunction(true,1), promFunction(false,2), promFunction(true,3)];
// list of all rejected promises
let proms3 = [promFunction(false,1), promFunction(false,2), promFunction(false,3)];
/// Promise.all()

Promise.all(proms1).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});

Promise.all(proms2).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});

Promise.all(proms3).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});
/* output
[
  'hey promise resolved 1',
  'hey promise resolved 2',
  'hey promise resolved 3'
]
you rejected the promise 2
you rejected the promise 1*/

Promise.allSettled ()

This method returns all the resolved or rejected data in an array as shown below

Promise.allSettled(proms1).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});

Promise.allSettled(proms2).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});

Promise.allSettled(proms3).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});
/*
[
  { status: 'fulfilled', value: 'hey promise resolved 1' },
  { status: 'fulfilled', value: 'hey promise resolved 2' },
  { status: 'fulfilled', value: 'hey promise resolved 3' }
]
[
  { status: 'fulfilled', value: 'hey promise resolved 1' },
  { status: 'rejected', reason: 'you rejected the promise 2' },
  { status: 'fulfilled', value: 'hey promise resolved 3' }
]
[
  { status: 'rejected', reason: 'you rejected the promise 1' },
  { status: 'rejected', reason: 'you rejected the promise 2' },
  { status: 'rejected', reason: 'you rejected the promise 3' }
]
*/

Promise.any()

any method returns the value of first resolved promise and if all promises are rejected, then returns an AggregateError.

Promise.any(proms1).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});

Promise.any(proms2).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});

Promise.any(proms3).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});
/*
hey promise resolved 1
hey promise resolved 1
[AggregateError: All promises were rejected] {
  [errors]: [
    'you rejected the promise 1',
    'you rejected the promise 2',
    'you rejected the promise 3'
  ]
}
*/

Promise.race()

returns the first settled promise, it might be resolved or rejected.

Promise.race(proms1).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});

Promise.race(proms2).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});

Promise.race(proms3).then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});
/*
hey promise resolved 1
hey promise resolved 1
you rejected the promise 1
*/

Async and Await

Async and Await are JS twin key words. The 'async' key word is used for a function to make it enable the property of asynchronous operations, but only when await is used inside it. That means, all the await lines are the only asynchronous lines and other lines are considered as synchronous lines. The "await" keyword is used for promises, this blocks all the next lines until promise is resolved. Look at the below example,

  function promFunction(foo,num) {
  return new Promise((resolve, reject) => {
    if (foo) {
      resolve("hey promise resolved "+num);
    } else {
      reject("you rejected the promise "+num);
    }
  });
}

async function asyncAwait(){
    try{
        let prom1 = await promFunction(true,1);
        console.log(prom1);

        let prom2 = await promFunction(true,2);
        console.log(prom2);

        let prom3 = await promFunction(false,3);
        console.log(prom3);

        let prom4 = await promFunction(true,4);
        console.log(prom4);

    }catch(error){
        console.log(error);
    }
}
asyncAwait();

/*
hey promise resolved 1
hey promise resolved 2
you rejected the promise 3
*/

here, try and catch blocks are used to handle errors, unlike in promises, we attach catch to handle errors. Using async and await, the code looks simpler and easier to debug.

Web Browser APIs

JavaScript extends some of the functionalities from browsers, which are called as web APIs. Some examples of web APIs

  • Console : Has methods to access the browser's debugging console.

  • Fetch : enables JS to fetch data from web servers.

  • LocalStorage : Saves the data in the local storage of browser, but the size is limited

  • Geolocation : Lets the users to provide their location to web application.