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

18 comments

  1. Thanks for sharing javascript code and this is helps us to solve the error we are getting, handle both synchronous and asynchronous errors with same try/catch and shows the complete error.

  2. Nice post! Thanks for sharing. Just my 2 cents, in the function ParallelMapFlow the async await code in the map is not necessary:

    should be this
    let results = jobs.map(job => doJob(job,job));
    instead of
    let results = jobs.map(async (job) => await doJob(job,job));

    And, in the function ParallelPromiseAllFlow, the await in line number 7 the same:

    should be
    finalResult += result;
    instead of
    finalResult += (await result);

    Thanks again,
    Mati

  3. Thanks for this. Just one simple question, I want to update the screen as run a loop in serial and this doesn’t work with the code I am using from your serial example. How can I get this to happen ?

  4. Nice article!!. Just one thing I think we don’t need here that is await on result for parallel execution using Promise.all.
    lasync 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) {
    // We can remove await here, Promise.all has already been executed, and given the result in an array. We just need to calculate the sum.
    finalResult += (await result);
    }
    console.log(finalResult);
    }
    ParallelPromiseAllFlow([1,2,3]);

  5. Interesting article, thanks for it, but with one bad practice. Although you think that you handled errors by adding try/catch it not truth. When job number 6 fails you says that in console is only “Error: test error” but there is also “UnhandledPromiseRejectionWarning: Error: test error” which you can not handle no way (except for next bad practice “process.on(‘unhandledRejection’, err => {})”). Why? Because it is not possible to handle errors using try/catch for multiple await in case two or more async parallel tasks. That is why Promise.all was designed. Always use Promise.all for two or more async parallel tasks. It is only one correct way how to handle errors. It means that you should never use something like that:
    let task1 = asyncTask1();
    let task2 = asyncTask2();
    let result1 = await task1;
    let result2 = await task2;

    You should replace your code:
    let results = promises.map(async (job) => await job);
    let finalResult = result1 + result2;
    for (const result of results) {
    finalResult += (await result);
    }

    with this code:
    let results = await Promise.all(promises);
    let finalResult = result1 + result2;
    for (const result of results) {
    finalResult += result;
    }

    and also remove useless try/catch from inside complexFlow() and handle errors this way:
    complexFlow().catch(err => { console.log(err); });

    or second alternative (if you really do not like promises syntax and prefer “sync style” using try/catch)
    (async function() { try { await complexFlow(); } catch(err) { console.log(err); }; })();

    Now there is no unhandled error in console.

    Using try/catch inside async function in this case has no sense because async functions by default return rejected promise when error is occured.

Leave a Reply

Your email address will not be published. Required fields are marked *