How AsyncContext Enables Seamless Data Propagation Across JavaScript Async Calls
The article introduces the TC39 Async Context proposal, explains why asynchronous context is needed, shows a library implementation using run and log functions, describes the AsyncContext API with run, get, and wrap methods, and explores use cases such as trace propagation, task‑priority handling, and comparisons with thread‑local storage and Node.js AsyncLocalStorage.
Background
Led by Alibaba TC39 representatives, the Async Context proposal became a TC39 Stage 1 proposal in early February 2023. Its goal is to define a way to pass data through JavaScript asynchronous tasks.
To illustrate why an async context is needed, imagine an npm library that provides
logand
runfunctions. Developers pass a callback and an id to
run, which invokes the callback and allows the callback to call
logto emit logs tagged with the id.
<code>// my-awesome-library
let currentId = undefined;
export function log(){
if (currentId === undefined) throw new Error('must be inside a run call stack');
console.log(`[${currentId}]`, ...arguments);
}
export function run<T>(id: string, cb: () => T){
let prevId = currentId;
try {
currentId = id;
return cb();
} finally {
currentId = prevId;
}
}
</code>Usage example:
<code>import { run, log } from 'my-awesome-library';
import { helper } from 'some-random-npm-library';
document.body.addEventListener('click', () => {
const id = nextId();
run(id, () => {
log('starting');
// assume helper calls doSomething
helper(doSomething);
log('done');
});
});
function doSomething(){
log("did something");
}
</code>For each click, the logs appear as:
[id1] starting [id1] did something [id1] doneThis demonstrates an id‑based mechanism that propagates through the synchronous call stack without requiring developers to manually pass or store the id, similar to how React Context passes parameters through component trees.
When asynchronous operations are introduced, the pattern breaks because the id is lost across async boundaries:
<code>document.body.addEventListener('click', () => {
const id = new Uuid();
run(id, async () => {
log('starting');
await helper(doSomething);
// This log can no longer print the expected id
log('done');
});
});
function doSomething(){
// Whether this log prints the expected id depends on whether helper awaited before calling doSomething
log("did something");
}
</code>The AsyncContext proposal solves this problem by allowing the id to be passed both through synchronous call stacks and asynchronous task chains.
<code>// my-awesome-library
const context = new AsyncContext();
export function log(){
const currentId = context.get();
if (currentId === undefined) throw new Error('must be inside a run call stack');
console.log(`[${currentId}]`, ...arguments);
}
export function run<T>(id: string, cb: () => T){
context.run(id, cb);
}
</code>AsyncContext
AsyncContext is a storage that can propagate any JavaScript value through a chain of synchronous and asynchronous operations. It provides three core methods:
<code>class AsyncContext<T> {
// Snapshot all AsyncContext values in the current execution context and return a function that restores the snapshot.
static wrap<R>(fn: (...args: any[]) => R): (...args: any[]) => R;
// Immediately execute fn while setting value as the current AsyncContext value; the value is snapshot for any async operations started inside fn.
run<R>(value: T, fn: () => R): R;
// Retrieve the current AsyncContext value.
get(): T;
}
</code> AsyncContext.prototype.run()writes a value,
get()reads it, and
wrap()snapshots the whole context so it can be restored later. These three operations form the minimal interface for propagating values across async tasks.
<code>// simple task queue example
const loop = {
queue: [],
addTask: (fn) => { queue.push(AsyncContext.wrap(fn)); },
run: () => { while (queue.length > 0) { const fn = queue.shift(); fn(); } }
};
const ctx = new AsyncContext();
ctx.run('1', () => {
loop.addTask(() => { console.log('task:', ctx.get()); });
setTimeout(() => { console.log(ctx.get()); }, 1000); // => 1
});
ctx.run('2', () => {
setTimeout(() => { console.log(ctx.get()); }, 500); // => 2
});
console.log(ctx.get()); // => undefined
loop.run(); // => task: 1
</code>Use Cases
Async Trace Propagation
APM tools like OpenTelemetry need to propagate trace data without requiring developers to modify business code. By storing trace information in an AsyncContext, the runtime can retrieve the current trace from the context at any point.
<code>// tracer.js
const context = new AsyncContext();
export function run(cb){
const span = {
parent: context.get(),
startTime: Date.now(),
traceId: randomUUID(),
spanId: randomUUID()
};
context.run(span, cb);
}
export function end(){
const span = context.get();
span?.endTime = Date.now();
}
const originalFetch = globalThis.fetch;
globalThis.fetch = (...args) => {
return run(() => originalFetch(...args).finally(() => end()));
};
</code>Application code remains unchanged; the tracer automatically wraps user callbacks.
Async Task Attribute Propagation
Scheduling APIs can use AsyncContext to automatically carry task attributes such as priority, eliminating the need for developers to pass them manually.
<code>// simple scheduler using AsyncContext
const scheduler = {
context: new AsyncContext(),
postTask(task, options){ this.context.run({ priority: options.priority }, task); },
currentTask(){ return this.context.get() ?? { priority: 'default' }; }
};
// user code
const res = await scheduler.postTask(task, { priority: 'background' });
async function task(){
const resp = await fetch('/hello');
const text = await resp.text();
scheduler.currentTask(); // => { priority: 'background' }
return doStuffs(text);
}
async function doStuffs(text){ return text; }
</code>This addresses a challenge identified by the WICG Scheduling APIs group.
Prior Arts
Thread‑Local Variables
Thread‑local storage provides per‑thread state without global interference, useful for re‑entrancy and request‑scoped data. Example in C++ shows each thread having its own
ragecounter.
<code>#include <iostream>
#include <thread>
#include <mutex>
thread_local unsigned int rage = 1;
std::mutex cout_mutex;
void increase_rage(const std::string& thread_name){
++rage;
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "Rage counter for " << thread_name << ": " << rage << '\n';
}
int main(){
std::thread a(increase_rage, "a"), b(increase_rage, "b");
a.join(); b.join();
{
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "Rage counter for main: " << rage << '\n';
}
return 0;
}
</code>Thread‑local storage is also used to keep request‑level information in server models.
AsyncLocalStorage
Node.js provides AsyncLocalStorage as a single‑threaded analogue of thread‑local storage. AsyncContext builds on top of it.
<code>class AsyncLocalStorage<T> {
constructor();
run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R;
getStore(): T;
}
class AsyncResource {
constructor();
runInAsyncScope<R>(fn: (...args: any[]) => R, thisArg, ...args: any[]): R;
}
</code>The AsyncContext API is expected to remain stable as the proposal progresses.
Noslate & WinterCG
Noslate Aworker, a member of the Web‑Interoperable Runtimes CG, is implementing a subset of AsyncLocalStorage that aligns with the future AsyncContext API, providing an early‑adoption path for runtimes like Cloudflare Workers and Deno.
More ECMAScript Proposals
The JavaScript Chinese Interest Group (JSCIG) invites contributions to ECMAScript discussions on GitHub.
References
Async Context proposal: https://github.com/tc39/proposal-async-context
React Context: https://reactjs.org/docs/context.html
OpenTelemetry: https://opentelemetry.io/
Scheduling APIs: https://github.com/WICG/scheduling-apis
Challenge in unified task model: https://github.com/WICG/scheduling-apis/blob/main/misc/userspace-task-models.md#challenges-in-creating-a-unified-task-model
Thread‑local variables: https://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B%E5%B1%80%E9%83%A8%E5%AD%98%E5%82%A8
errno: http://man7.org/linux/man-pages/man3/errno.3.html
Noslate Aworker: https://noslate.midwayjs.org/docs/noslate_workers/intro
Web‑Interoperable Runtimes CG: https://wintercg.org/
AsyncLocalStorage subset: https://github.com/wintercg/proposal-common-minimum-api/blob/main/asynclocalstorage.md
Taobao Frontend Technology
The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.