{"id":18137762,"url":"https://github.com/tc39/proposal-async-context","last_synced_at":"2025-05-15T02:07:53.730Z","repository":{"id":59359192,"uuid":"252708440","full_name":"tc39/proposal-async-context","owner":"tc39","description":"Async Context for JavaScript","archived":false,"fork":false,"pushed_at":"2025-04-11T12:42:52.000Z","size":572,"stargazers_count":662,"open_issues_count":26,"forks_count":17,"subscribers_count":63,"default_branch":"master","last_synced_at":"2025-04-14T02:59:14.936Z","etag":null,"topics":["javascript"],"latest_commit_sha":null,"homepage":"https://tc39.es/proposal-async-context/","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"cc0-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tc39.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"COPYING.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-04-03T11:08:00.000Z","updated_at":"2025-04-12T17:24:53.000Z","dependencies_parsed_at":"2024-01-13T02:29:00.298Z","dependency_job_id":"de5411b4-8b0e-4dfe-b0f2-ae5ff0e27482","html_url":"https://github.com/tc39/proposal-async-context","commit_stats":null,"previous_names":["legendecas/proposal-async-context"],"tags_count":0,"template":false,"template_full_name":"legendecas/tc39-proposal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-async-context","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-async-context/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-async-context/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tc39%2Fproposal-async-context/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tc39","download_url":"https://codeload.github.com/tc39/proposal-async-context/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254259383,"owners_count":22040820,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript"],"created_at":"2024-11-01T15:06:33.811Z","updated_at":"2025-05-15T02:07:53.674Z","avatar_url":"https://github.com/tc39.png","language":"HTML","funding_links":[],"categories":["HTML"],"sub_categories":[],"readme":"# Async Context for JavaScript\n\nStatus: Stage 2\n\nChampions:\n\n- Andreu Botella ([@andreubotella](https://github.com/andreubotella))\n- Chengzhong Wu ([@legendecas](https://github.com/legendecas))\n- Justin Ridgewell ([@jridgewell](https://github.com/jridgewell))\n\nDiscuss with the group and join the bi-weekly via [#tc39-async-context][]\nmatrix room ([Matrix Guide][]).\n\n# Motivation\n\nWhen writing synchronous JavaScript code, a reasonable expectation from\ndevelopers is that values are consistently available over the life of the\nsynchronous execution. These values may be passed explicitly (i.e., as\nparameters to the function or some nested function, or as a closed over\nvariable), or implicitly (extracted from the call stack, e.g., outside the scope\nas a external object that the function or nested function has access to).\n\n```javascript\nfunction program() {\n  const value = { key: 123 };\n\n  // Explicitly pass the value to function via parameters.\n  // The value is available for the full execution of the function.\n  explicit(value);\n\n  // Explicitly captured by the closure.\n  // The value is available for as long as the closure exists.\n  const closure = () =\u003e {\n    assert.equal(value.key, 123);\n  };\n\n  // Implicitly propagated via shared reference to an external variable.\n  // The value is available as long as the shared reference is set.\n  // In this case, for as long as the synchronous execution of the\n  // try-finally code.\n  try {\n    shared = value;\n    implicit();\n  } finally {\n    shared = undefined;\n  }\n}\n\nfunction explicit(value) {\n  assert.equal(value.key, 123);\n}\n\nlet shared;\nfunction implicit() {\n  assert.equal(shared.key, 123);\n}\n\nprogram();\n```\n\nAsync/await syntax improved in ergonomics of writing asynchronous JS. It allows\ndevelopers to think of asynchronous code in terms of synchronous code. The\nbehavior of the event loop executing the code remains the same as in a promise\nchain. However, passing code through the event loop loses _implicit_ information\nfrom the call site because we end up replacing the call stack. In the case of\nasync/await syntax, the loss of implicit call site information becomes invisible\ndue to the visual similarity to synchronous code -- the only indicator of a\nbarrier is the `await` keyword. As a result, code that \"just works\" in\nsynchronous JS has unexpected behavior in asynchronous JS while appearing almost\nexactly the same.\n\n```javascript\nfunction program() {\n  const value = { key: 123 };\n\n  // Implicitly propagated via shared reference to an external variable.\n  // The value is only available only for the _synchronous execution_ of\n  // the try-finally code.\n  try {\n    shared = value;\n    implicit();\n  } finally {\n    shared = undefined;\n  }\n}\n\nlet shared;\nasync function implicit() {\n  // The shared reference is still set to the correct value.\n  assert.equal(shared.key, 123);\n\n  await 1;\n\n  // After awaiting, the shared reference has been reset to `undefined`.\n  // We've lost access to our original value.\n  assert.throws(() =\u003e {\n    assert.equal(shared.key, 123);\n  });\n}\n\nprogram();\n```\n\nThe above problem existed already in promise callback-style code, but the\nintroduction of async/await syntax has aggravated it by making the stack\nreplacement almost undetectable. This problem is not generally solvable with\nuser land code alone. For instance, if the call stack has already been replaced\nby the time the function is called, that function will never have a chance to\ncapture the shared reference.\n\n```javascript\nfunction program() {\n  const value = { key: 123 };\n\n  // Implicitly propagated via shared reference to an external variable.\n  // The value is only available only for the _synchronous execution_ of\n  // the try-finally code.\n  try {\n    shared = value;\n    setTimeout(implicit, 0);\n  } finally {\n    shared = undefined;\n  }\n}\n\nlet shared;\nfunction implicit() {\n  // By the time this code is executed, the shared reference has already\n  // been reset. There is no way for `implicit` to solve this because\n  // because the bug is caused (accidentally) by the `program` function.\n  assert.throws(() =\u003e {\n    assert.equal(shared.key, 123);\n  });\n}\n\nprogram();\n```\n\nFurthermore, the async/await syntax bypasses the userland Promises and\nmakes it impossible for existing tools like [Zone.js](#zonesjs) that\n[instruments](https://github.com/angular/angular/blob/main/packages/zone.js/STANDARD-APIS.md)\nthe `Promise` to work with it without transpilation.\n\nThis proposal introduces a general mechanism by which lost implicit call site\ninformation can be captured and used across transitions through the event loop,\nwhile allowing the developer to write async code largely as they do in cases\nwithout implicit information. The goal is to reduce the mental burden currently\nrequired for special handling async code in such cases.\n\n## Summary\n\nThis proposal introduces APIs to propagate a value through asynchronous code,\nsuch as a promise continuation or async callbacks.\n\nCompared to the [Prior Arts][prior-arts.md], this proposal identifies the\nfollowing features as non-goals:\n\n1. Async tasks scheduling and interception.\n1. Error handling \u0026 bubbling through async stacks.\n\n# Proposed Solution\n\n`AsyncContext` is designed as a value store for context propagation across\nlogically-connected sync/async code execution.\n\n```typescript\nnamespace AsyncContext {\n  class Variable\u003cT\u003e {\n    constructor(options: AsyncVariableOptions\u003cT\u003e);\n    get name(): string;\n    get(): T | undefined;\n    run\u003cR\u003e(value: T, fn: (...args: any[])=\u003e R, ...args: any[]): R;\n  }\n  interface AsyncVariableOptions\u003cT\u003e {\n    name?: string;\n    defaultValue?: T;\n  }\n\n  class Snapshot {\n    constructor();\n    run\u003cR\u003e(fn: (...args: any[]) =\u003e R, ...args: any[]): R;\n    static wrap\u003cT, R\u003e(fn: (this: T, ...args: any[]) =\u003e R): (this: T, ...args: any[]) =\u003e R;\n  }\n}\n```\n\n## `AsyncContext.Variable`\n\n`Variable` is a container for a value that is associated with the current\nexecution flow. The value is propagated through async execution flows, and\ncan be snapshot and restored with `Snapshot`.\n\n`Variable.prototype.run()` and `Variable.prototype.get()` sets and gets\nthe current value of an async execution flow.\n\n```typescript\nconst asyncVar = new AsyncContext.Variable();\n\n// Sets the current value to 'top', and executes the `main` function.\nasyncVar.run(\"top\", main);\n\nfunction main() {\n  // AsyncContext.Variable is maintained through other platform queueing.\n  setTimeout(() =\u003e {\n    console.log(asyncVar.get()); // =\u003e 'top'\n\n    asyncVar.run(\"A\", () =\u003e {\n      console.log(asyncVar.get()); // =\u003e 'A'\n\n      setTimeout(() =\u003e {\n        console.log(asyncVar.get()); // =\u003e 'A'\n      }, randomTimeout());\n    });\n  }, randomTimeout());\n\n  // AsyncContext.Variable runs can be nested.\n  asyncVar.run(\"B\", () =\u003e {\n    console.log(asyncVar.get()); // =\u003e 'B'\n\n    setTimeout(() =\u003e {\n      console.log(asyncVar.get()); // =\u003e 'B'\n    }, randomTimeout());\n  });\n\n  // AsyncContext.Variable was restored after the previous run.\n  console.log(asyncVar.get()); // =\u003e 'top'\n}\n\nfunction randomTimeout() {\n  return Math.random() * 1000;\n}\n```\n\n\u003e [!TIP]\n\u003e There have been long detailed discussions on the dynamic scoping of\n\u003e `AsyncContext.Variable`. Checkout [SCOPING.md][] for more details.\n\nHosts are expected to use the infrastructure in this proposal to allow tracking\nnot only asynchronous callstacks, but other ways to schedule jobs on the event\nloop (such as `setTimeout`) to maximize the value of these use cases. We\ndescribe the needed integration with web platform APIs in the [web integration\ndocument](./WEB-INTEGRATION.md).\n\nA detailed example of use cases can be found in the\n[Use Cases](./USE-CASES.md) and [Frameworks](./FRAMEWORKS.md) documents.\n\n## `AsyncContext.Snapshot`\n\n`AsyncContext.Snapshot` is an advanced API that allows opaquely capturing\nthe current values of all `Variable`s, and execute a function at a later\ntime as if those values were still the current values.\n\n`Snapshot` is useful for implementing APIs that logically \"schedule\" a\ncallback, so the callback will be called with the context that it logically\nbelongs to, regardless of the context under which it actually runs:\n\n```typescript\nlet queue = [];\n\nexport function enqueueCallback(cb: () =\u003e void) {\n  // Each callback is stored with the context at which it was enqueued.\n  const snapshot = new AsyncContext.Snapshot();\n  queue.push(() =\u003e snapshot.run(cb));\n}\n\nrunWhenIdle(() =\u003e {\n  // All callbacks in the queue would be run with the current context if they\n  // hadn't been wrapped.\n  for (const cb of queue) {\n    cb();\n  }\n  queue = [];\n});\n```\n\nMost web developers, even those interacting with `AsyncContext.Variable` directly,\nare not expected to ever need to reach out to `AsyncContext.Snapshot`.\n\n\u003e [!TIP]\n\u003e A detailed explanation of why `AsyncContext.Snapshot` is a requirement can be\n\u003e found in [SNAPSHOT.md](./SNAPSHOT.md).\n\nNote that even with `AsyncContext.Snapshot`, you can only access the value associated with\na `AsyncContext.Variable` instance if you have access to that instance. There is no way to\niterate through the entries or the `AsyncContext.Variable`s in the snapshot.\n\n```typescript\nconst asyncVar = new AsyncContext.Variable();\n\nlet snapshot\nasyncVar.run(\"A\", () =\u003e {\n  // Captures the state of all AsyncContext.Variable's at this moment.\n  snapshot = new AsyncContext.Snapshot();\n});\n\nasyncVar.run(\"B\", () =\u003e {\n  console.log(asyncVar.get()); // =\u003e 'B'\n\n  // The snapshot will restore all AsyncContext.Variable to their snapshot\n  // state and invoke the wrapped function. We pass a function which it will\n  // invoke.\n  snapshot.run(() =\u003e {\n    // Despite being lexically nested inside 'B', the snapshot restored us to\n    // to the snapshot 'A' state.\n    console.log(asyncVar.get()); // =\u003e 'A'\n  });\n});\n```\n\n### `AsyncContext.Snapshot.wrap`\n\n`AsyncContext.Snapshot.wrap` is a helper which captures the current values of all\n`Variable`s and returns a wrapped function. When invoked, this wrapped function\nrestores the state of all `Variable`s and executes the inner function.\n\n```typescript\nconst asyncVar = new AsyncContext.Variable();\n\nfunction fn() {\n  return asyncVar.get();\n}\n\nlet wrappedFn;\nasyncVar.run(\"A\", () =\u003e {\n  // Captures the state of all AsyncContext.Variable's at this moment, returning\n  // wrapped closure that restores that state.\n  wrappedFn = AsyncContext.Snapshot.wrap(fn)\n});\n\n\nconsole.log(fn()); // =\u003e undefined\nconsole.log(wrappedFn()); // =\u003e 'A'\n```\n\nYou can think of this as a more convenient version of `Snapshot`, where only a\nsingle function needs to be wrapped. It also serves as a convenient way for\nconsumers of libraries that don't support `AsyncContext` to ensure that function\nis executed in the correct execution context.\n\n```typescript\n// User code that uses a legacy library\nconst asyncVar = new AsyncContext.Variable();\n\nfunction fn() {\n    return asyncVar.get();\n}\n\nasyncVar.run(\"A\", () =\u003e {\n    defer(fn); // setTimeout schedules during \"A\" context.\n})\nasyncVar.run(\"B\", () =\u003e {\n    defer(fn); // setTimeout is not called, fn will still see \"A\" context.\n})\nasyncVar.run(\"C\", () =\u003e {\n    const wrapped = AsyncContext.Snapshot.wrap(fn);\n    defer(wrapped); // wrapped callback captures \"C\" context.\n})\n\n\n// Some legacy library that queues multiple callbacks per macrotick\n// Because the setTimeout is called a single time per queue batch,\n// all callbacks will be invoked with _that_ context regardless of\n// whatever context is active during the call to `defer`.\nconst queue = [];\nfunction defer(callback) {\n    if (queue.length === 0) setTimeout(processQueue, 1);\n    queue.push(callback);\n}\nfunction processQueue() {\n    for (const cb of queue) {\n        cb();\n    }\n    queue.length = 0;\n}\n```\n\n\n# Examples\n\n## Determine the initiator of a task\n\nApplication monitoring tools like OpenTelemetry save their tracing spans in the\n`AsyncContext.Variable` and retrieve the span when they need to determine what started\nthis chain of interaction.\n\nThese libraries can not intrude the developer APIs for seamless monitoring. The\ntracing span doesn't need to be manually passing around by usercodes.\n\n```typescript\n// tracer.js\n\nconst asyncVar = new AsyncContext.Variable();\nexport function run(cb) {\n  // (a)\n  const span = {\n    startTime: Date.now(),\n    traceId: randomUUID(),\n    spanId: randomUUID(),\n  };\n  asyncVar.run(span, cb);\n}\n\nexport function end() {\n  // (b)\n  const span = asyncVar.get();\n  span?.endTime = Date.now();\n}\n```\n\n```typescript\n// my-app.js\nimport * as tracer from \"./tracer.js\";\n\nbutton.onclick = (e) =\u003e {\n  // (1)\n  tracer.run(() =\u003e {\n    fetch(\"https://example.com\").then((res) =\u003e {\n      // (2)\n\n      return processBody(res.body).then((data) =\u003e {\n        // (3)\n\n        const dialog = html`\u003cdialog\u003e\n          Here's some cool data: ${data} \u003cbutton\u003eOK, cool\u003c/button\u003e\n        \u003c/dialog\u003e`;\n        dialog.show();\n\n        tracer.end();\n      });\n    });\n  });\n};\n```\n\nIn the example above, `run` and `end` don't share same lexical scope with actual\ncode functions, and they are capable of async reentrance thus capable of\nconcurrent multi-tracking.\n\n## Transitive task attribution\n\nUser tasks can be scheduled with attributions. With `AsyncContext.Variable`, task\nattributions are propagated in the async task flow and sub-tasks can be\nscheduled with the same priority.\n\n```typescript\nconst scheduler = {\n  asyncVar: new AsyncContext.Variable(),\n  postTask(task, options) {\n    // In practice, the task execution may be deferred.\n    // Here we simply run the task immediately.\n    return this.asyncVar.run({ priority: options.priority }, task);\n  },\n  currentTask() {\n    return this.asyncVar.get() ?? { priority: \"default\" };\n  },\n};\n\nconst res = await scheduler.postTask(task, { priority: \"background\" });\nconsole.log(res);\n\nasync function task() {\n  // Fetch remains background priority by referring to scheduler.currentTask().\n  const resp = await fetch(\"/hello\");\n  const text = await resp.text();\n\n  scheduler.currentTask(); // =\u003e { priority: 'background' }\n  return doStuffs(text);\n}\n\nasync function doStuffs(text) {\n  // Some async calculation...\n  return text;\n}\n```\n\n## User-land queues\n\nUser-land queues can be implemented with `AsyncContext.Snapshot` to propagate\nthe values of all `AsyncContext.Variable`s without access to any of them. This\nallows the user-land queue to be implemented in a way that is decoupled from\nconsumers of `AsyncContext.Variable`.\n\n```typescript\n// The scheduler doesn't access to any AsyncContext.Variable.\nconst scheduler = {\n  queue: [],\n  postTask(task) {\n    // Each callback is stored with the context at which it was enqueued.\n    const snapshot = new AsyncContext.Snapshot();\n    queue.push(() =\u003e snapshot.run(task));\n  },\n  runWhenIdle() {\n    // All callbacks in the queue would be run with the current context if they\n    // hadn't been wrapped.\n    for (const cb of this.queue) {\n      cb();\n    }\n    this.queue = [];\n  }\n};\n\nfunction userAction() {\n  scheduler.postTask(function userTask() {\n    console.log(traceContext.get());\n  });\n}\n\n// Tracing libraries can use AsyncContext.Variable to store tracing contexts.\nconst traceContext = new AsyncContext.Variable();\ntraceContext.run(\"trace-id-a\", userAction);\ntraceContext.run(\"trace-id-b\", userAction);\n\nscheduler.runWhenIdle();\n// The userTask will be run with the trace context it was enqueued with.\n// =\u003e 'trace-id-a'\n// =\u003e 'trace-id-b'\n```\n\n# FAQ\n\n## Are there any prior arts?\n\nPlease checkout [prior-arts.md][] for more details.\n\n## Why take a function in `run`?\n\nThe `Variable.prototype.run` and `Snapshot.prototype.run` methods take a\nfunction to execute because it ensures async context variables\nwill always contain consistent values in a given execution flow. Any modification\nmust be taken in a sub-graph of an async execution flow, and can not affect\ntheir parent or sibling scopes.\n\n```typescript\nconst asyncVar = new AsyncContext.Variable();\nasyncVar.run(\"A\", async () =\u003e {\n  asyncVar.get(); // =\u003e 'A'\n\n  // ...arbitrary synchronous codes.\n  // ...or await-ed asynchronous calls.\n\n  // The value can not be modified at this point.\n  asyncVar.get(); // =\u003e 'A'\n});\n```\n\nThis increases the integrity of async context variables, and makes them\neasier to reason about where a value of an async variable comes from.\n\n## How does `AsyncContext` interact with built-in schedulers?\n\nAny time a scheduler (such as `setTimeout`, `addEventListener`, or\n`Promise.prototype.then`) runs a user-provided callback, it must choose which\nsnapshot to run it in. While userland schedulers are free to make any choice\nhere, this proposal adopts a convention that built-in schedulers will always run\ncallbacks in the snapshot that was active when the callback was passed to the\nbuilt-in (i.e. at \"registration time\"). This is equivalent to what would happen\nif the user explicitly called `AsyncContext.Snapshot.wrap` on all callbacks\nbefore passing them.\n\nThis choice is the most consistent with the function-scoped structure that\nresults from `run` taking a function, and is also the most clearly-defined\noption among the possible alternatives.  For instance, many event listeners\nmay be initiated either programmatically or through user interaction; in the\nformer case there may be a more recently relevant snapshot available, but it's\ninconsistent across different types of events or even different instances of the\nsame type of event. On the other hand, passing a callback to a built-in function\nhappens at a very clearly defined time.\n\nAnother advantage of registration-time snapshotting is that it is expected to\nreduce the amount of intervention required to opt out of the default snapshot.\nBecause `AsyncContext` is a subtle feature, it's not reasonable to expect every\nweb developer to build a complete understanding of its nuances. Moreover, it's\nimportant that library users should not need to be aware of the nature of the\nvariables that library implementations are implicitly passing around. It would\nbe harmful if common practices emerged that developers felt they needed to wrap\ntheir callbacks before passing them anywhere. The primary means to have a\nfunction run in a different snapshot is to call `Snapshot.wrap`, but this\nwill be idempotent when passing callbacks to built-ins, making it both less\nlikely for this common practice to begin in the first place, and also less\nharmful when it does happen unnecessarily.\n\n## What if I need access to the snapshot from a more recent cause?\n\nThe downside to registration-time snapshotting is that it's impossible to opt\n_out_ of the snapshot restoration to access whatever the snapshot would have\nbeen _before_ it was restored. Use cases where this snapshot is more relevant\ninclude\n\n- programmatically-dispatched events whose handlers are installed at application\n  initialization time\n- unhandled rejection handlers are a specific example of the above\n- tracing execution flow, where one task \"follows from\" a sibling task\n\nAs explained above, the alternative snapshot choices are much more specific to\nthe individual use case, but they can be made available through side channels.\nFor instance, web specifications could include that certain event types will\nexpose an `originSnapshot` property (actual name to be determined) on the event\nobject containing the active `AsyncContext.Snapshot` from a specific point in\ntime that initiated the event.\n\nProviding these additional snapshots through side channels has several benefits\nover switching to them by default, or via a generalized \"previous snapshot\"\nmechanism:\n\n- different types of schedulers may have a variety of potential origination\n  points, whose scope can be matched precisely with a well-specified side\n  channel\n- access via a known side channel avoids loss of idempotency when callbacks are\n  wrapped multiple times (whereas a \"previous snapshot\" would becomes much less\n  clear)\n- no single wrapper method for developers to build bad habits around\n\n[`asyncresource.runinasyncscope`]:\n  https://nodejs.org/dist/latest-v14.x/docs/api/async_hooks.html#async_hooks_asyncresource_runinasyncscope_fn_thisarg_args\n[#tc39-async-context]: https://matrix.to/#/#tc39-async-context:matrix.org\n[Matrix Guide]: https://github.com/tc39/how-we-work/blob/main/matrix-guide.md\n[solution.md]: ./SOLUTION.md\n[scoping.md]: ./SCOPING.md\n[prior-arts.md]: ./PRIOR-ARTS.md\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftc39%2Fproposal-async-context","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftc39%2Fproposal-async-context","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftc39%2Fproposal-async-context/lists"}