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
}
The signature of the async flavoured function could be written following:
([...any]): Promise<any>
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
}
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')
}
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' }
Similar behaviour would be this:
function myFunction() {
return Promise.resolve('hello world')
}
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
comparing to standard function:
function mySyncFunction() { return myPromise }
var p = myFunction()
// p is holding Promise { 42 }
p === myPromise // true
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
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')
the above code will result in the following output
before
serviceA:1
serviceB:1
after
resolving...
serviceB:2
serviceA:2
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')
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()
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)
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)
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)
}
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)
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)
}
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)
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)
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.
Top comments (0)