DEV Community

webduvet
webduvet

Posted on • Edited on

Async - Await

Async Await keywords

  • how does async-await work
  • how does it compare to a Promise
  • examples of usage
  • pitfalls

In this article I'm going to explore and explain how does async-await structure work.
What is the impact on the code and how does it compare to standard javascript Promise.
Then I'm going to demonstrate on couple of examples how the code looks when using Promise and how does it look with async-await keywords.
I'm going to mention few pitfalls and tricky parts when using both coding styles.
I'm going to give the links to relevant documentation and specification.

Introduction

Async - await was introduced in Ecmascript specification in 2017 with the goal of simplifying asynchronous flow.

Basic principles and rules

The asynchronous function is defined with the keyword async, like this:

async myFunction() {
  // body of the function
}
Enter fullscreen mode Exit fullscreen mode

The signature of the async flavoured function could be written following:

([...any]): Promise<any>
Enter fullscreen mode Exit fullscreen mode

async function can be called from anywhere, however the use of await keyword is permitted only from within async block.

async myFirstFunction() {
  // some logic
  const partial = await getParialResult(); // calling another async function or function returning promise
  // other logic
  return processPartial(partial) // calling sync function with non promise parameter returning non promise value
}
Enter fullscreen mode Exit fullscreen mode

the part some logic is executed synchronously. The part other logic is executed asynchronously only after
the async function call getParialResult is resolved.

Relationship with promises

The difference between standard and the async flavoured function is that the async function always returns javascript Promise object.
There are few basic rules around this.

The return statement is not defined

Where the standard function returns undefined value, the async function returns Promise<undefined> - Promise resolved to undefined.

async myFunction() {
  console.log('hi from async function')
}
Enter fullscreen mode Exit fullscreen mode

The function returns (not thenable) value

If the return statement is present and the return value is not a Promise and not undefined, the value is wrapped in the resolved Promise
and returned.

async function myFunction() {
  ...
  return 'hello world'
}

myFunction() // Promise { 'hello world' }
Enter fullscreen mode Exit fullscreen mode

Similar behaviour would be this:

function myFunction() {
  return Promise.resolve('hello world')
}
Enter fullscreen mode Exit fullscreen mode

The function return thenable value promise or promise like object

The last case is only a subset of the previous case, however it deserves a special mention.
The async function returns Promise . In this case the interpreter does similar thing again with one subtle but important difference.
Promise.resolve will automatically flatten any nested layers if "thenable" object is found. This is not the case of async function return. Here the value wrapped inside promise is unwrapped and wrapped again in new Promise object.

Comparing to Promise.resolve:

const myPromise = new Promise((resolve, reject) => { resolve(42) });
async function myAsyncFunction() { return myPromise }

var p = myFunction()
// p is holding Promise { 42 }

p === myPromise // false
myPromise === Promise.resolve(myPromise) // true, because the nested structure is flattened
Enter fullscreen mode Exit fullscreen mode

comparing to standard function:

function mySyncFunction() { return myPromise }

var p = myFunction()
// p is holding Promise { 42 }

p === myPromise // true
Enter fullscreen mode Exit fullscreen mode

Should we simulate the behaviour of returning value wrapped in resolved Promise from async function we could write:

function likeAsyncFunction() {
  // value inside promise is unwrapped and wrapped again in new promise object
  return myPromise.then(value => Promise.resolve(value))
}
p = likeAsyncFunction() // Promise { 42 }

myPromise === p // false
Enter fullscreen mode Exit fullscreen mode

So, is it only syntactic sugar?

The first think what crossed my mind was hold on, this is just syntactic sugar for promises. Whatever exists after await keyword could go into then handler. Is this true?
Few examples ilustrates similarities and differences to promises and perhaps gives you some ideas or notions how to explore async-await structure beyond promises.

Synchrounous and asynchronous part

I'll ilustrate the nature of typical async function on the following example. It can be executed in nodejs.

// app.js
// run node app.ja

/*
 * this function will be used trhought few more examples, so keep it.
 * when using plain promises the async keyword can be ignored (ref. to the above explanation)
 */
async function sleep(mls) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('resolving...')
      resolve(mls)
    }, mls)
  })
}

async function serviceB() {
  console.log('serviceB:1');
  await sleep(1000)
  console.log('serviceB:2')
}

async function serviceA() {
  console.log('serviceA:1')
  await serviceB()
  console.log('serviceA:2')
}

console.log('before')
serviceA();
console.log('after')
Enter fullscreen mode Exit fullscreen mode

the above code will result in the following output

before
serviceA:1
serviceB:1
after
resolving...
serviceB:2
serviceA:2
Enter fullscreen mode Exit fullscreen mode

serviceA is called (pushed in stack) as regular function. Execution continues as synchronous.
Inside serviceA it gets to the first await keyword with function call to serviceB. Now this function serviceB is analyzed and executed.
It is pushed to the stack and executed synchronously until either returns (Promise) or until another await function call is found.
What happened to the rest of the function after await call?
It is considered as another block of code similar to callback. The block is queued and pushed back to stack once the async operation is finished.

This is a very close equivalent using Promises:

function serviceB() {
  console.log('serviceB:1');
  return new Promise(resolve => {
    sleep(1000).then(() => {
      console.log('serviceB:2')
      resolve();
    })
  })
}

function serviceA() {
  console.log('serviceA:1')
  return new Promise((resolve) => {
    serviceB().then(() => {
      console.log('serviceA:2')
      resolve();
    })
  })
}

console.log('before')
serviceA();
console.log('after')
Enter fullscreen mode Exit fullscreen mode

Running it the exact same way as the previous code will yield exactly same output. The console log demonstrates how the both
function serviceA and serviceB gets to stack and then leave the stack allowing to execute console.log('after').
Once the async part is finished the callback, or the block of code after async is placed on stack and serviceB is executed, after that callback or block after async of serviceA is placed on the stack and executed.

Besides how it does work these two examples also demonstrate one of the early mentioned benefits of async-await constructs.
The code is more readable and less cluttered with the callbacks.
However, some might argue that synchronous nature of the syntax might produce confusion and some hard to trace bugs.
What I mean by this?

serviceA()
serviceB()
serviceC()
Enter fullscreen mode Exit fullscreen mode

If these are all async functions with await inside, the order in which the await part of the functions complete is independent to the order how these functions are called.
Writing this in the traditional way might better promote the actual behaviour.

serviceA().then(callbackA)
serviceB().then(callbackB)
serviceC().then(callbackC)
Enter fullscreen mode Exit fullscreen mode

It is always good to learn how do things work to avoid future confusion.

FOR loop and similar

treating async code in for loop, particularly when the callback need to run in a sequence can be challenging.
It looks all plain and simple when using async-await

async function update(earliestVersion, lastVersion)
{
  for (i = earliestVersion; i <= lastVersion, i++) {
    try {
      await applyUpdate(`version_${first}`);
    } catch(e) {
      throw Error('Update Error')
    }
  }
}

// possible usage in the code:
update(12, 16)
  .then(handleSuccess)
  .catch(handleError)
  .finally(handleFinish)
Enter fullscreen mode Exit fullscreen mode

The promise based alternative could work perhaps something like this.
You can already see that it is not that clear how the logic flows, not to mention where and how to handle the exceptions and failures.

function update(earliestVersion, lastVersion) {
  function _update(version){
    return applyUpdate(version)
      .then((res) => {
        if (version <= lastVersion) {
          return _update(version + 1)
        } else {
          return res;
        }
      })
      .catch(() => { throw Error('Update Error') })
  }
  return _update(version)
}
Enter fullscreen mode Exit fullscreen mode

WHILE loop and similar

This is similar case as the for loop. Let's say we are running the hub for wind farm and the server is asking wind turbine to report the status.
In case of sever weather the server need to keep asking for the wind turbine status until the status is retrieved or until number of max tries is reached and alarm is raised.

async function reportStatus(nu) {
  let status = false;
  let tries = 0;
  while (!status) {
    await status = getTurbineStatus(nu)
    logStatusCall(no, status, tries++)
  }
  return status;
}
// usage
turbines.forEach(reportStatus)

// or
Promses.allSettled(turbines.map(reportStatus))
.then(handleResponses)
Enter fullscreen mode Exit fullscreen mode

Similar to for loop this will be more challenging to write and test using Promises

function reportStatus(nu) {
  let status = false;
  let tries = 0;
  function _helper(n){
    return getTurbineStatus(n).then((status) => {
        logStatusCall(no, status, tries++)
        if (!status) {
          return _helper(n);
        } else {
          return status
        }
      })
  }
  return _helper(nu)
}
Enter fullscreen mode Exit fullscreen mode

How about generator function*?

Is it possible to combine generator function with async keyword? Yes and no to a certain extent.
Here is the example of simple countdown function. It is using setTimeout.

async function* countdown(count, time) {
    let index = count;

    while (index) {
        await sleep(time)
        yield --index;
    }
}

async function testCountDown(count) {
  const cd = countdown(4, 1000)
  let val = await cd.next();
  while (!val.done) {
    console.log(`finish in ${val.value}`)
    val = await cd.next();
  }
  console.log('...finished')
}

testCountDown(5)
Enter fullscreen mode Exit fullscreen mode

Comparing to synchronous generator function there is one key difference. It actually breaks the iteration protocols (without await).
Async function always returns a Promise, so the expected object { value, done } is wrapped in the Promise.
It also would not work in for..of loop neither it will work with spread operator [...iterable].
Both constructs expects iterable and the interpreter can't access the { value, done } object directly.
My advice is don't use the async generator functions - if you really have to use them be aware of differences to avoid unexpected behavior and bugs.

async function as a method

Method is a function bound to an object. So how does async function work as a method, and how does it compare to traditional function returning promise?
Async function simplifies the flow here as well. Unlike promise in promise handler keyword this refers to the calling object even in the async part of the block which following after await keyword. To refer to this from inside the promise handler we need to use arrow functions or to bind this.

example:

function logName() {
  console.log(`Hi, my name is ${this.name}.`)
}

class Simpson {
  constructor(name) {
    this.name = name
  }
  logName() {
    console.log(`Hi, my name is ${this.name}.`)
  }
  async waitAndSayHi(time) {
    await sleep(time);
    this.logName();
  }
  waitAndSayHiWithPromise(time) {
    return new Promise(resolve => {
      sleep(time).then(this.logName.bind(this))
    })
  }
}

const lisa = new Simpson('Lisa')
const bart = new Simpson('Bart')

lisa.waitAndSayHi(500)
bart.waitAndSayHiWithPromise(1000)
Enter fullscreen mode Exit fullscreen mode

Omitting .bind(this) will result in the obvious error for obvious reasons. Something we don't need to worry about when using async-await.

Summary

async - await is a handy way how to tackle the asynchronous code. It helps with flow control and it is particularly useful in loops when multiple sequence of asynchronous operations is required.
It improves readability of the code provided the programmer is fully aware of the consequences.
It should be seen as an extension to promise architecture rather then as merely syntactic sugar for promises.

Sources

  1. Async Function Definition
  2. Generator
  3. Async-Await MDN

Top comments (0)