Understanding async/await in JavaScript
async/await is a mechanism in JavaScript that makes working with asynchronous IO easier. But it seems to be hard to understand. In this article, I will introduce my intuitive understanding of async/await.
Idea
In our daily work, our tasks may contain not only the action part but also the waiting part.
Sometimes, we need to wait for an event such as the response from someone else or our other task to be finished before the task can continue.
To improve efficiency, we may have multiple tasks in progress at the same time. If the current task reaches the waiting part, we may switch to another task, and go back later. In this way, we make use of the waiting time of one task by doing other tasks. We can also save time by waiting for multiple events at the same time.
The concept of asynchronous and concurrent are based on this idea. If some tasks are in progress at the same time, we say these tasks are executed concurrently. We call the task that can be executed concurrently with others an asynchronous task. In JavaScript, most of the IO operations, the operations whose major parts are “waiting”, are asynchronous. The tranditional IO function starts a new asynchronous task that does the IO operation then executes the call-back function. The caller continue to execute once the IO operation reaches the waiting part. The caller may start another asynchronous IO operation which is executed concurrently with other unfinished IO operations.
However, in traditional JavaScript, it is not convienient to perform an asynchronous IO operation and wait for it in the middle of a task. We have to split the task into many call-back functions, which can make the code hard to write and hard to understand. The async/await feature is to solve this issue. We introduce promise object to represent an asynchronous task. We can mark the function that starts an asynchronous task and returns a promise with async
. Inside the function, we implement the task, and we can use await
on a promise
object to wait for a the corresponding task and get its output. The a
in await
indicates that the waiting is for asynchronous task, which means, we will try to switch to another in-progress asynchronous task while waiting. [1]
Promise
As mentioned earlier, we use a promise object to represent an asynchronous task that has started. At a certain time, a promise can be
- resolved with a value, i.e. a successful result,
- rejected with a reason, i.e. failed for a reason,
- not finished yet.
In TypeScript, the promise has a Promise<T>
type where T
is the resolve value type.
Beside accepting a call-back function, an asynchronous IO function can return a promise object to represent the IO task. There are some libraries providing such API, e.g. fs/promises
. You can also use new Promise(...)
[2] to turn a call-back style API to a promise.
async/await
Now we want to build a more complex asynchronous task that includes waiting for some other tasks. It’s where async/await becomes useful. You can mark the function or lambda with async
keyword to indicate that it starts a new task. The async
keyword has the following effects.
- The function always returns a promise, representing the started task. In TypeScript, the return type has to be
Promise<T>
whereT
can be any type. - The function body becomes what the task does. In the function body,
- the value returned become the promise resolve value.
- the error thrown become the promise reject reason.
- you can use
await
on a promise object, which waits for the task, either giving you the resolved value or throwing the reject reason.
Here is an example of async
and await
.
1 | const assert = require("assert"); |
If you want to use await
at top-level, you can create an async
lambda and immediately execute it, like this
1 | (async () => { |
To elaborate that the tasks are indeed executed concurrently, let’s create an example that starts 1000 tasks of performing an IO operation twice. Here we use setTimeout
to simulate a long-waiting IO.
1 | function longIO() { |
Here we execute task
function but do not await
on it. This makes it starts the 1000 tasks immediately. Running this code, you should notice that after roughly 2 seconds, 1000 Task #n has finished its first longIO
printed, then after roughly 2 seconds, 1000 Task #n has finished its second longIO
printed. This shows that the tasks are executed concurrently. They wait for the longIO
at the same time and they are in progress at the same time.
Summarize
This article briefly introduces the idea of async/await and discusses how to achieve concurrency. Because I’m too lazy I don’t want this article to be too long, I didn’t go into all the details. I hope this article dispels your fear of async/await.
Explainations used in this article
- concurrent: multiple tasks are executed concurrently if they are being progressed at the same time.
- asynchronous: the task that can be executed concurrently with other tasks.
- promise: an asynchronous task that has started. It may be in progress, resolved, or rejected.
- async: the keyword to mark the function that starts an asynchronous task and returns a promise. You can use await in the function body.
- await: wait for a promise to finish.
DISCLAIMER: this explaination of the name
await
is unofficial. ↩︎For more information, check the document of Promise constructor. ↩︎