May 13, 2018

JavaScript Async/Await: Serial, Parallel and Complex Flow

If you have experience on ASP.NET MVC then probably you are familiar with async/await keywords in C#. The same thing is now in JavaScript. Before that, Callbacks and Promises are used for asynchronous code. Async/await is built on top of promises and non blocking. The power of Async/await provides asynchronous code look like synchronous code. In this article, we will go through serial/chain and parallel flow and implement a complex flow using Async/Await.

Read Also: JavaScript Promises: Understanding Error Handling with Example

Async/Await Structure

Consider following function which returns a Promise:


function myPromise(){
    return new Promise(function(resolve, reject) {
    // do your async Job here
    if (/* all is well */) {
        resolve("Done!");
    }
    else {
        reject(Error("Failed!"));
    }
    });
}

Here is structure to use Async/await


async function myAsyncFunc() {
    try{
        let result  = await myPromise();
        console.log(result);
    }
   catch(err) {
     console.log(err);
    }
}
myAsyncFunc();

Points To Note:

- async keyword is used before function. It means the function always returns a promise even it returns non-promise in it.

- The keyword await is used before calling Promise function, it makes JavaScript wait until that promise settles and returns its result.

- The error is handled by regular try...catch. In case of Promise reject, it jumps to the catch block.

- The await keyword can only be used inside functions defined with async means we can’t use await in the top level of our code since that is not inside an async function.

Consider following promise function:


function doJob(x,sec) {
  return new Promise(resolve => {
  console.log('Start: ' + x);
    setTimeout(() => {
    	console.log('End: ' + x);
      resolve(x);
    }, sec *1000);
  });
}

We are passing x and time duration in seconds as argument and it returns the same after the given duration.

Serial or Chaining Flow

If you want to execute different promise methods sequential i.e. execute one by one, it is easy using Async/Await and the code looks like synchronous.


async function SerialFlow(){

let result1 = await doJob(1,1);
let result2 = await doJob(2,2);
let result3 = await doJob(3,3);

let finalResult = result1+result2+result3;
console.log(finalResult);
return finalResult;

}

SerialFlow();

In above method first 1 is executed and wait for one sec then 2 is started wait for 2 seconds after that 3 started to execute. 1,2,3 are executed sequentially, hence the total time is 6 seconds.

Console Output:


Start: 1
End: 1
Start: 2
End: 2
Start: 3
End: 3
6

You can do the same thing with loop:


async function SerialLoopFlow(jobs) {
  let finalResult = 0;
  for (const job of jobs) {
    let result = await doJob(job,job); 
    finalResult += result;
  }
  console.log(finalResult);
}
SerialLoopFlow([1,2,3]);

The output will be same.

You can do the same operation with Array.reduce also:


async function SerialReduceFlow(jobs) {

let finalResult = await jobs.reduce(async (total,job) =>{		
		return await total +  await doJob(job,job);
},0);

console.log(finalResult);
}

SerialReduceFlow([1,2,3]);

Parallel or Concurrent Flow

If you want to execute the same in parallel or concurrently:


async function ParallelFlow(){

let result1 =  doJob(1,1);
let result2 =  doJob(2,2);
let result3 =  doJob(3,3);

let finalResult =await result1+ await result2+ await result3;

console.log(finalResult);
return finalResult;
}

ParallelFlow();

All started concurrently, 1 is finished in one sec, 2 & 3 are finished in two & three seconds respectively. The total time is 3 seconds.

Console Output:


 Start: 1
 Start: 2
 Start: 3
 End: 1
 End: 2
 End: 3
 6

If you think that all await are in same line that's why it executed concurrently, you are wrong. see below function:


async function ParallelFlow(){

let result1 =  doJob(1,1);
let result2 =  doJob(2,2);
let result3 =  doJob(3,3);

let r1 = await result1;
let r2 = await result2;
let r3 = await result3;

let finalResult =r1+ r2+r3;

console.log(finalResult);
return finalResult;
}

ParallelFlow();

The above is same and executes all three in parallel. So make a point, if you use await doJob(1,1) then it happens sequential if you assign it to a variable and then await the variable then it executes in parallel.

For an array, you can use Map to run it in parallel:


async function ParallelMapFlow(jobs) {

let results = jobs.map(async (job) => await doJob(job,job));
let finalResult = 0;
for (const result of results) {
    finalResult += (await result);
  }
 console.log(finalResult);
}

ParallelMapFlow([1,2,3]);

The output is same and all happens in 3 seconds. You can use Promise.all also with await.


async function ParallelPromiseAllFlow(jobs) {

let promises = jobs.map((job) => doJob(job,job));
let results =  await Promise.all(promises) 
let finalResult = 0;
for (const result of results) {
    finalResult += (await result);
  }
 console.log(finalResult);
}
ParallelPromiseAllFlow([1,2,3]);

All above 4 methods, outputs are same and run concurrently.

Complex Flow

In real world implementation, the requirement is not straight. It is combination of serial and parallel flow. Let's take a look at below flow:

Javascript async await

1 and 2 are in series then 3,4,5 and series of 6,7 are in parallel. Let's implement it using async/await.

As 1 and 2 are in series:


  let result1 = await doJob(1, 1);
  let result2 = await doJob(2, 1);

6 and 7 are in series


    let Flow6_7 = async function() {
      let result6 = await doJob(6, 1);
      let result7 = await doJob(7, 1);
      return result6 + result7;
    }

Now 3,4,5 and Flow6_7 are in parallel


    
    let promises = [doJob(3, 2), doJob(4, 2), doJob(5, 2), Flow6_7()];
    let results = promises.map(async (job) => await job);
   

As 6 and 7 are one sec each, took 2 seconds duration for 3,4,5 so that all could end up on same time.

Let's calculate sum of all methods


    let finalResult = result1 + result2;
    for (const result of results) {
      finalResult += (await result);
    }
    console.log(finalResult);

To handle error, use try catch. Here is the complete method:


async function complexFlow() {
  try {

    let result1 = await doJob(1, 1);
    let result2 = await doJob(2, 1);

    let Flow6_7 = async function() {
      let result6 = await doJob(6, 1);
      let result7 = await doJob(7, 1);
      return result6 + result7;
    }

    let promises = [doJob(3, 2), doJob(4, 2), doJob(5, 2), Flow6_7()];
    let results = promises.map(async (job) => await job);
    let finalResult = result1 + result2;
    for (const result of results) {
      finalResult += (await result);
    }
    console.log(finalResult);
    return finalResult;

  } catch (err) {
    console.log(err);
  }
}

complexFlow();

Here is the output in console:

Javascript output flow

To test error handling, let's throw error in a particular condition:


function doJob(x, sec) {
if (x == 6) {
    throw Error('test error');
  } 
  return new Promise(resolve => {
    console.log('Start: ' + x);
    setTimeout(() => {
      console.log('End: ' + x);
      resolve(x);
    }, sec * 1000);
  });
}

then the console output will have the following:


Error: test error
    at doJob ((index):69)
    at Flow6_7 ((index):87)
    at complexFlow ((index):92)

Conclusion

Async/await really simplifies the life to deal with Promises and also makes to handle both synchronous and asynchronous errors with same try/catch and shows the complete error stack which helps a lot to debug the error. Node 8 LTS is with full Async/Await support. Have you started to use? Would you like to share your experience?

Happy JavaScript and Node.js