Open-source project
tc39/proposal-async-context avatar
tc39/proposal-async-context

Async Context for JavaScript: carrying a value across the await

Async Context for JavaScript

881 stars26 forksHTMLCC0-1.0

At a glance

What is it?
The tc39 Async Context proposal is at Stage 2 and introduces APIs that implicitly propagate a value through asynchronous code, so a value captured at a call site survives the event loop instead of being reset by the next tick. The proposal is built around an AsyncContext object in the core specification plus a separate story for how the value propagates through web platform APIs.
Who is it for?
Follow this proposal if you build tracing, debugging or framework instrumentation for JavaScript, because it removes the class of bug where an await silently discards a captured value, and the API shape is settled enough to prototype against. Do not build a production dependency on it yet, since Stage 2 means the design can still change and the propagation rules for web APIs live in a separate document.
Can I use it commercially?
Yes. CC0-1.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly HTML, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The bug that only appears after an await

The proposal starts from a reasonable expectation: in synchronous JavaScript, values are consistently available over the life of the synchronous execution. They can be passed explicitly, as parameters to a function or a nested function, or captured by a closure. They can also be propagated implicitly, extracted from the call stack, for instance as an external object that the function has access to. That second kind is the fragile one, because it depends on a shared reference staying set. In synchronous code, a try-finally block that assigns the shared reference and clears it in the finally works fine. The proposal's second example shows what happens when the same pattern meets an await.

javascript
let shared;
async function implicit() {
  // The shared reference is still set to the correct value.
  assert.equal(shared.key, 123);

  await 1;

  // After awaiting, the shared reference has been reset to `undefined`.
  // We've lost access to our original value.
  assert.throws(() => {
    assert.equal(shared.key, 123);
  });
}

The try-finally in the caller has already run by the time the continuation resumes, so the reference has been cleared and the value is gone.

Async/await hid the problem it made worse

Async and await improved the ergonomics of asynchronous JavaScript and let developers think of asynchronous code in terms of synchronous code. The behaviour of the event loop did not change; it remains the same as in a promise chain. What changed is that passing code through the event loop loses implicit information from the call site, because the call stack is replaced. With promise callbacks that loss was visible, because the callback shape told you a barrier had been crossed. With async and await the loss becomes almost invisible, because the code looks synchronous and the only indicator of a barrier is the await keyword. The proposal's phrasing is that code which just works in synchronous JavaScript has unexpected behaviour in asynchronous JavaScript while appearing almost exactly the same. The problem existed already in promise callback-style code, but the introduction of async and await aggravated it by making the stack replacement almost undetectable.

Why user land code cannot patch this

The proposal is explicit that the problem is not generally solvable with user land code alone, and gives the reason: if the call stack has already been replaced by the time the function is called, that function will never have a chance to capture the shared reference. The third example in the proposal shows the case where the caller uses setTimeout, so the deferred function runs on a later tick, after the finally block has already cleared the shared variable. The comment in that example is blunt about the ownership of the bug, saying it is caused accidentally by the program function, so the deferred function has no way to solve it. The proposal also names an existing casualty: the async and await syntax bypasses userland Promises, which makes it impossible for existing tools like Zone.js that instrument the Promise to work with it without transpilation. A general mechanism in the platform is therefore presented as the only route to a fix that works for code the developer did not write.

Two parts: a core API and a web integration story

The proposed solution has two parts, and the split reflects where authority lies. The first is the API interface users interact with, the AsyncContext object, which is intended to become part of the core ECMAScript specification. The second is propagation across async APIs. The proposal notes that ECMA-262 defines only a couple of async APIs, namely Promise and FinalizationRegistry, while most others are defined by web specifications, and the way propagation should work is described in a separate document on web integration. That second part is the harder half and the one most likely to shape the final API, because the platform has to opt in one API at a time. The proposal names two components of the API, AsyncContext.Variable and AsyncContext.Snapshot, and the repository root carries a separate snapshot document describing that half in more detail, alongside a scoping document and a mutation scope document that set the boundaries of what the API may do.

What the proposal explicitly refuses to do

Two items are listed as non-goals, and they are worth noting because they define where the boundary with other proposals sits. The first is async task scheduling and interception, which is the territory of Zone.js style patching of the event loop. The second is error handling and bubbling through async stacks, which is a separate concern from carrying a value. The list is framed as being about similar APIs in other languages, so the proposal is distinguishing itself from prior art rather than claiming the territory is unclaimed. This matters when you evaluate the proposal against a library you already use: a tracing tool that needs to intercept scheduling will not be satisfied by Async Context, because scheduling interception is on the other side of that boundary.

The stated motivation is instrumentation, not developer convenience

Two goals are named. The first is to reduce the mental burden currently required for special handling async code in cases where implicit information matters, by letting developers write async code largely as they do in cases without it. The second is to lower the barrier for developers instrumenting production applications to debug real user problems, enabling the use of tracing tools to collect additional performance metrics and information about code flows. The proposal describes that as a widespread approach in other programming languages which has so far not been possible on the web because of the asynchronous nature of many web APIs. The framing is that this is about observability rather than ergonomics. For front-end frameworks, the stated use is to reduce and automate boilerplate around attributing async code to specific components, which the proposal calls a source of hard to debug issues. Concrete use cases live in two separate documents, one abstract and one covering popular front-end frameworks.

A spec repository with a real build

The repository is a specification source rather than an implementation, and it is set up like one. The entry point is spec.html, and the package manifest shows the toolchain: the build script runs ecmarkup with the tc39 ecma262 bibliography loaded, in a loose mode that writes into a build directory and a strict mode that passes an additional flag, with a watch script for iterating. The lint script runs the TypeScript compiler against the project, and the test script runs mocha. The devDependencies pin a specific set, including ecmarkup at 21.3.1, mocha at 10.2.0, typescript at 4.9.4, and the esbuild-kit loaders used to run the tests. There is also a shadowrealm-biblio.json at the root, which is the bibliography file for the ShadowRealm proposal referenced by the spec text. The tests directory and the src directory sit alongside the spec, and a mocharc and a prettier configuration cover the JavaScript sources. The licence is CC0-1.0, which is the customary choice for specification text.

Where the proposal stands and where it is going

The status line reads Stage 2, and the repository has no tagged releases, which is normal for a specification proposal: what changes is the text, not a versioned package. The champions listed are Andreu Botella, Chengzhong Wu and Justin Ridgewell, and the group meets in a bi-weekly Matrix room with the tc39-async-context tag. The homepage points at the tc39 proposal site, where the rendered specification lives. The last push to the repository was on 2026-06-16. The repository also carries a set of documents that show where the design work is going: a continuation document, a memory management document, a mutation scope document, a scoping document, the snapshot document, the prior arts document, and the use case and framework documents. The prior arts document is the one to read if you want to know how this compares to the equivalent mechanisms in other languages, since the README points at it for exactly that comparison.

Editorial conclusion

Follow this proposal if you build tracing, debugging or framework instrumentation for JavaScript, because it removes the class of bug where an await silently discards a captured value, and the API shape is settled enough to prototype against. Do not build a production dependency on it yet, since Stage 2 means the design can still change and the propagation rules for web APIs live in a separate document. Verify first which parts are settled, by reading the scoping and mutation-scope documents that set out what the API may and may not do, and note that the proposal explicitly lists async task scheduling and interception, and error handling through async stacks, as non-goals.

Frequently asked questions

What is the Async Context proposal for JavaScript?

It introduces APIs to implicitly propagate a value through asynchronous code, such as a promise continuation or async callbacks. The goal is to capture call site information that the event loop discards, so a value captured before an await is still available after it.

What stage is the Async Context proposal at?

The status line in the repository reads Stage 2. The champions are Andreu Botella, Chengzhong Wu and Justin Ridgewell, and the group discusses in a bi-weekly Matrix room with the tc39-async-context tag.

Which APIs does the Async Context proposal define?

The proposal consists of an AsyncContext object intended to become part of the core ECMAScript specification, with AsyncContext.Variable and AsyncContext.Snapshot named as its components, plus a separate mechanism for propagating across the async APIs defined by web specifications, described in a separate web integration document.

What does the Async Context proposal explicitly leave out?

Two things are listed as non-goals: async task scheduling and interception, and error handling and bubbling through async stacks. The proposal names similar APIs in other languages as its comparison point.

Why does an await break a shared variable in JavaScript?

Because the event loop replaces the call stack, and a try-finally in the caller clears the shared reference before the continuation resumes. With async and await this loss is nearly invisible because the only indicator of the barrier is the await keyword, and the proposal argues user land code cannot solve it because the function is called after the stack has already been replaced.

Official sources

  1. Issues
  2. License: CC0-1.0
  3. Project website
  4. README
  5. tc39/proposal-async-context on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tc39-proposal-async-context.svg)](https://hysenlabs.com/projects/tc39-proposal-async-context)

Was this useful?