Could JavaScript have synchronous await?

[2025-03-28] dev, javascript
(Ad, please don’t block)

In JavaScript, code has color: It is either synchronous or asynchronous. In this blog post, we explore:

  • The problems caused by that
  • How to fix them via synchronous await
  • The two downsides that prevent synchronous await from being practical

The problems of asynchronous code being different from synchronous code  

The 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.

More information on code color and asynchronicity in JavaScript  

Can we remove the limitations of code color?  

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.

Asynchronous 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:

  • An execution context is related to a stack frame.
  • Code evaluation state is related to a program counter.

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().

Synchronous 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.

What are the consequences of synchronous 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

Performance is affected negatively  

One downside is that performance would degrade considerably

  • In async functions, each await affects performance negatively because the code must be resumable and (e.g) local variables can’t be kept in registers.
  • With synchronous await, every function call must be resumable and performance suffers accordingly.

Storing and restoring full stacks is bad for performance, too.

Concurrency issues become important  

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
  }
}

A few more random thoughts  

  • Syntactically, we may need the keyword sync to enable the keyword await in functions.
  • Promises are still needed as the foundation of await.
  • There is a proposal for stack switching in WebAssembly.

Further reading  

Acknowledgements: Thanks to Thomas Broyer, Ashley Claymore and Heribert Schütz for their feedback.