Chaining With Async & Await

ASYNC-AWAIT : Makes everything look simple as synchronous code

As we have seen earlier, the callback approach has the problem of nesting the follow up processing; resulting into complex nested code. The Promise significantly makes it cleaner with .then() blocks which looked more sequential.

The async-await which has been built on the promise code, smartly improves the syntax, making it look like synchronous code.

Besides improving the readability, it also makes it easy for debugging as the sequence of execution is as we see in the code.

So, let us look at few examples to understand it further.

How does it work?

The below diagram shows a snippet of an async function highlighting the key behavior of the async and await key words.

Async-Await : How it works ?

An async function always returns a promise and can have multiple promise calls as shown. We can use await only inside an async function

The await smartly simplifies the syntax by replacing the .then() clause. If we ignore the async & await key words, the code looks like a completely synchronous code.

What exactly is happening inside in await?

The use of await suspends the program execution till the response arrives. It’s just similar to a plain promise call suspending the program till the response triggers the .then().

Importantly, if we compare it to waiting on a synchronous calls, there the thread may sit idle while waiting for the response. But, with await (or even then()) the executing thread continues with it’s other tasks while the program waits for the response to arrive.

Here is the complete code for the snippet shown above and we are running the async function twice.

const echoService = (msg)=>{ //A promise function that echos after 1sec.
                        return new Promise(resolve => 
                        setTimeout(()=>resolve(msg), 1000))};
const errService = ()=>{// A promise function returning a reject after 1sec.   																						 
                        return new Promise((resolve,reject) => 
                        setTimeout(()=>reject("Ooops..."), 1000))};


async function demoAsyncFunction() {
   try{  
     let response = await echoService("say hello");
     console.log("echoService :" + response);

     let errResponse = await errService(); //This await throws an error
     console.log("mockReject :" + errResponse); //this will be skipped.
   
   }catch(err){
          console.log("Error :"+err);
   }  
}

demoAsyncFunction();
demoAsyncFunction();

/*Output (each with 1sec delay):
echoService :say hello
echoService :say hello
Error :Ooops...
Error :Ooops...
*/

As we can observer from the output:

  • First, both the calls are running in parallel in a non blocking manner.
  • Second, the second await is throwing an error which we are handling inside the catch block.
    • Thus the await returns a value or throws an error similar to a synchronous function. In addition, what appears as waiting in the response, it smartly handles it by suspending the program.

Exploring Different Aspects with Examples

Example-1 : Comparing against the promise code

As highlighted below it makes 2 asynchronous promise calls with following steps :

  1. Write some content to test.txt.
  2. Read the content when writing is complete.
  3. Print the content to console as the read completes.
const fs = require('fs').promises;
const content = 'Some content!';

async function testWriteRead(){
  try {
    await fs.writeFile('test.txt', content);
    
    let data = await fs.readFile('test.txt', 'utf8');
    console.log("Content after write : \n" + data);    
    
  } catch (err) {
    console.error(err)
  }
}
testWriteRead();
const fs = require('fs').promises;
const filePath='test.txt';
const content = 'Some content!';

fs.writeFile(filePath, content, 'utf8')
.then(()=>{
  
 return fs.readFile(filePath, 'utf8');
})
.then((data)=>{
  console.log("Content after write : \n" + data);  
})
.catch((error) => {
  console.error(error);
});

console.log("Chaining calls with promise : \n");

The highlighted code shows how async/await replaces the .then() clauses to make it simple and look like a synchronous code.

Example-2 : Within promise execution sequence could be confusing

While we mix promise code with other synchronous code, the execution sequence could be confusing and difficult to debug. We can easily avoid this using async\await.

Promise.resolve()
.then(()=>{
    console.log("say hello")
    Promise.resolve("say hello again!")
           .then((data)=>{console.log(data);});
    console.log("Promise call completed !");
});

/*Output: Line No 3 => 6 => 5
say hello
Promise call completed !
say hello again!
*/
async function myAsynAwait() {    
    console.log("say hello")
    const data = await Promise.resolve("say hello again!");
    console.log(data);
    console.log("Async call completed !");
}
myAsynAwait();


/*Outcome : Line No =>3 =>4 =>5

say hello
say hello again!
Promise call completed !

*/
Example-3 : Using await on Promise all, race, any for parallel calls.

While running promises in parallel we can make use of the promise apis such as Promise.all\any\race.

Since all of them return a promise, we can use await before them as shown below.

const p1 = new Promise((resolve)=>setTimeout(resolve, 1000, '==>Saving Account Balance'));
const p2 = new Promise((resolve)=>setTimeout(resolve, 2000, '==>Lastest trasactions'));
const p3 = new Promise((resolve)=>setTimeout(resolve, 600, '==>Fixed Deposite Balance'));

async function fetchAccountSummary(){
  
  //Run in parallel using await
  const results =  await Promise.all([p1,p2,p3]);
  console.log("Async :\n"+results);
  
  //Run in parallel using await
  Promise.all([p1,p2,p3])
  .then((resultArr)=>
        console.log("Promise :\n"+resultArr));  
}  
  
fetchAccountSummary();
/*Output :
Async :
==>Saving Account Balance,==>Lastest trasactions,==>Fixed Deposite Balance
Promise :
==>Saving Account Balance,==>Lastest trasactions,==>Fixed Deposite Balance
*/

Example-4 : Async function always returns a promise.

As discussed above an async function always returns a promise. Even if we return some value the function automatically converts them into a promise value. The below test functions shows the various return scenarios.

async function asyncOne() {
  console.log("Executed asyncOne.....")
}

asyncOne();
asyncOne().then(()=>console.log("Yes, it returns a promise.."))

/*Output :
Executed asyncOne.....
Executed asyncOne.....
Yes, it returns a promise..
*/
async function asyncTwo() {
  console.log("\nExecuted asyncTwo.....");
  return "asyncTwoResult";
}
asyncTwo().then((data)=>console.log("asyncTwo "+data));

/*Output :
Executed asyncTwo.....
asyncTwo asyncTwoResult
*/
async function asyncThree() {
  console.log("\nExecuted asyncThree.....");
  throw "asyncThree failed.";
}
asyncThree().then((data)=>console.log("asyncThree : "+data),
                  (err)=>console.log("asyncThree err: "+err)
                  );

/*Output :
Executed asyncThree.....
asyncThree err: asyncThree failed.
*/