await
?In JavaScript, code has color: It is either synchronous or asynchronous. In this blog post, we explore:
await
await
from being practicalThe key problem is that synchronous code can’t call asynchronous code. On one hand that introduces a lot of duplication:
Some functionality has to be implemented twice – e.g., iterator methods such as .filter()
exist in synchronous and asynchronous versions.
There are both synchronous and asynchronous iterables.
We have a synchronous for-of
loop for synchronous iterables and an asynchronous ´for-of-await` loop for asynchronous iterables.
There are synchronous generators and asynchronous generators.
We have synchronous and asynchronous versions of ordinary functions, arrow functions and methods.
On the other hand, it can pose intractable problems for APIs – e.g.: Say that you write a synchronous parser for Markdown. After it’s almost finished, you realize that for syntax-highlighting, you can’t load language definitions on demand because that is asynchronous functionality. Similarly, plugins for the parser can’t do anything asynchronously. You could make the parser asynchronous but then you’d lose the convenience of a synchronous API.
Blog post by Anthony Fu: “Async, Sync, in Between”
Tool that lets you write “uncolored” code once and produces synchronous and asynchronous code: quansync. However, even there, you can’t call async code within sync code.
Four chapters on asynchronous programming in “Exploring JavaScript”:
All of the aforementioned problems would go away if we could use await
in synchronous code. To understand how that could work, we must first understand asynchronous await
.
await
This is asynchronous code that uses await
:
async function f() {
await g();
}
async function g() {
await h();
}
async function h() {
await fetch('https://example.com');
}
In each of these functions, await
is used to suspend and resume execution. The ECMAScript specification uses execution contexts to enable this functionality – they contain “code evaluation state”: “Any state needed to perform, suspend, and resume evaluation of the code associated with this execution context.”
Loose analogies:
What happens if await
is executed – e.g., as follows?
await value;
These are the steps (source):
// • `asyncContext` is current execution context (of surrounding
// async function).
Promise.resolve(value).then(
(result) => {
// • Suspend current execution context.
// • Push `asyncContext` onto execution context stack.
// • Resume `asyncContext` with `result` as result of `await`.
},
// Omitted: error handling
);
// • Remove `asyncContext` from stack and suspend it.
// • Omitted: remaining steps
Give that each of the asynchronous functions f()
, g()
and h()
stores one execution context, all of the call stack is recorded when we await fetch()
in h()
.
await
Can we do what we did in the previous subsection synchronously?
function f() {
g();
}
function g() {
h();
}
function h() {
await fetch('https://example.com'); // (A)
}
If JavaScript were multi-threaded, the current thread could block in line A.
However, we don’t need multi-threading to enable this functionality: h()
could store and remove the complete (synchronous) execution context stack ECS
when it is suspended and restore ECS
when it is resumed.
await
? The usability improvements of synchronous await
would be significant – e.g., we wouldn’t need asynchronous iterables and for-await-of
anymore because normal iterables could block any time they needed to.
In contrast to the problematic synchronous XMLHttpRequest, synchronous await
would not block the main thread.
Alas, there are two, probably fatal, downsides
One downside is that performance would degrade considerably
await
affects performance negatively because the code must be resumable and (e.g) local variables can’t be kept in registers.await
, every function call must be resumable and performance suffers accordingly.Storing and restoring full stacks is bad for performance, too.
With synchronous await
, any function call (such as the one in line A) could pause a function such as an event handler:
function handleEvent() {
// Start with an operation
libraryFunction(); // (A)
// Continue with the operation
}
That could mess up the operation, so you’d need (e.g.):
function handleEvent() {
mutex {
// Start with an operation
libraryFunction();
// Continue with the operation
}
}
sync
to enable the keyword await
in functions.await
.Acknowledgements: Thanks to Thomas Broyer, Ashley Claymore and Heribert Schütz for their feedback.