OK so we gushed over the many benefits of the async/await syntax, and already started touching on a few key points of both async and await.
Let's now get to the nitty-gritty, starting with exploring what an async function actually is, and how it behaves.
It implicitly returns a promise
For starters, let me repeat that an async function returns a promise. It is intrinsic and automatic. Symptomatically, this is about the same as if its body was in a return Promise.resolve().then().
This means that an uncaught exception in there will return a rejected promise. But if no exception is thrown, the function's potential return value becomes the fulfilled value of the returned promise.
Do note that you can qualify as async both a traditional function (declared with the function keyword) and an arrow function. There's no impact difference across these.
Code example: 04-async-functions-return-promises.js
To try and drive this point home, check out this code: even though the sayWhat() function simply returns its argument, calling it does produce a promise, as our first console.log() demonstrates.
So we can call then() on it, in the customary way.
Same for uncaught exceptions, that turn into rejected promises you can use with, for instance, a catch() call farther down the chain.
So yeah, sayWhat() here has no good reason to use the async qualifier; after all, there's no await in there. But this serves well to illustrate our point with minimal code.
Allows await… in its immediate body
When a function is qualified as async, this allows you, at the syntax level, to use the await keyword in its immediate body.
I can't stress this enough: the await keyword cannot appear just anywhere “inside” the async function. Most importantly, it's not transitively allowed in nested functions such as callbacks or local function declarations.
Another way of saying it, reversing our perspective, is that await is only allowed when the closest surrounding function is qualified as async.
Code example: 05-async-scopes.js
This little bit of code illustrates this: here's an await that is not in the immediate body of thisCanWait(), but in a callback inside that body, and that callback is not qualified as async (also it doesn't need to be, and that wouldn't do what we want).
This would trigger what we call early errors, syntax errors, which means the entire file will refuse to run, we won't even get that first "Shall we?" log, see for yourself. (demo run)
No await? No async.
Conversely, you can intuit that there's almost no point qualifying as async a function that doesn't use await.
If that's just to skip having to manually call Promise.resolve() in a return, that's bit lazy, and can result in some confusion for readers of your code. Including you, three months down the line.
Even worse, if your function already returns a promise, this will just wrap it in an extra promise. It sure works, but that's needlessly heavy. We'll see linter rules that help us avoid that.
Code example: 06-awaitless-async-is-useless.js
This tiny code shows that, yes (demo run), you can qualify a function as async without using await, but again, that's generally just making your code bigger and its execution costlier for no good reason.