Ebenezer Don Full-stack software engineer with a passion for building meaningful products that ease the lives of users.

Understanding async context and the future of server-side JavaScript

7 min read 2198 105

Understanding async context and the future of server-side JavaScript

JavaScript has made its mark as one of the most versatile and widely used languages in programming. Whether you’re developing a snappy, interactive webpage or crafting a robust, scalable backend for your web application, JavaScript has the tools and libraries you need. But, like all languages, it certainly has its quirks and challenges. One such challenge is handling asynchronous operations, a vital aspect of programming.

Asynchronous programming enables JavaScript to perform tasks in a non-blocking way, meaning it doesn’t have to wait for one operation to complete before moving on to the next. It’s a powerful tool but can also cause tricky problems, especially when managing context between asynchronous calls.

This issue is where the new async context proposal steps in to save the day. This article provides a comprehensive guide to understanding the async context proposal, its implications for server-side JavaScript, and what it might mean for the future of JavaScript development.

Below is a list of what we’ll cover in this post; if you’re familiar with the history of async JavaScript, I recommend skipping straight to What is the Async Context API? to get the most from this post.

Jump ahead:

Understanding asynchronous JavaScript code

Before we delve into the async context proposal, let’s ensure we’re on the same page about what asynchronous code means.

At its simplest, asynchronous programming is a design pattern that allows a program to continue executing other tasks while it waits for an operation to complete. This design pattern starkly contrasts with synchronous programming, where the program executes each task in order, and only moves on to the next task when the current one is finished.

JavaScript is a single-threaded language, which means it can only do one thing at a time. However, it needs to handle many operations, like network requests, timers, and more, that don’t fit neatly into a single, linear sequence of tasks. That’s where the event loop comes in.

The event loop

The event loop is an essential component of JavaScript’s environment that enables asynchronous JavaScript. It continually cycles through the call stack and checks for tasks to execute.

Suppose a task isn’t ready for execution, such as when it is waiting for data from a server — the event loop can move on to the next task and return to the waiting task once it’s ready. This way, JavaScript can handle multiple tasks over time without stopping or blocking the rest of the program.

JavaScript offers several techniques for writing asynchronous code. Earlier versions of JavaScript relied on callbacks, where one function would be passed into another function and run at a later time. This approach, however, resulted in the notorious “callback hell,” a situation with deeply nested callbacks that left code hard to read and maintain.

JavaScript promises and async/await

To solve this problem, JavaScript introduced promises, which represent the eventual outcome (success or failure) of an asynchronous operation and its resulting value. Promises helped to flatten nested callbacks and make asynchronous code easier to work with.

They also enabled the use of async/await syntax, further improving the readability of asynchronous JavaScript code. Through async/await, developers can write asynchronous code that reads much like synchronous code, making it easier to understand data flow and events.

However, while these tools have made asynchronous JavaScript much more manageable, they still have some issues. One of the main problems is the loss of implicit context when passing code through the event loop. Let’s delve into this challenge in the next section.

The challenge of asynchronous context

As we’ve mentioned, JavaScript’s event loop — combined with promises and async/await — allows developers to write asynchronous code that is far easier to manage and understand than nested callbacks. However, these tools present their own difficulties: when passing code through the event loop, certain implicit information from the call site is lost.

What is this “implicit information”? In synchronous code execution, values are constantly available during the lifecycle of the execution. They can be passed around explicitly, like function parameters or closed-over variables (which are defined in an outer scope but referenced by an inner function), or implicitly, such as values stored in a variable accessible to multiple scopes.

However, things change when it comes to asynchronous execution. Let’s illustrate this with an example. Suppose you have a variable shared among different function scopes, and its value changes during the execution of an asynchronous function.

At the beginning of the function’s execution, the shared variable may hold a certain value. But by the time the function is complete (for instance, after an await operation), the value of that shared variable may have changed, leading to potentially unexpected outcomes.

This is where the problem of losing the implicit call site information arises in asynchronous JavaScript. The behavior of the event loop remains the same whether you’re using promise chains or async/await.

However, the call stack — the mechanism that keeps track of function calls and where to return after a function call — gets modified as the event loop executes async code. This means that information that was implicitly available at the original call site may be lost by the time an async function completes.

The introduction of async/await syntax, while overall a positive change, has further complicated this issue. Because async/await makes asynchronous code look so similar to synchronous code, it’s even harder to see where the call stack — and the context — has been modified. This can lead to bugs and unpredictable behavior — and unexpected bugs that are difficult to track down.

Recognizing this problem, members of the TC39 committee, Chengzhong Wu and Justin Ridgewell have sought a way to preserve context across asynchronous operations. Their solution is the Async Context API.

What is the Async Context API?

The Async Context API is a powerful new mechanism for handling context in asynchronous JavaScript code presented in the Async Context proposal. The proposal is currently in Stage 2 of the ECMAScript standardization process as of August 2023, which means it has been approved as a draft proposal and is now under active development. Although the proposal is still subject to change, it is considered a viable candidate for inclusion in the next version of JavaScript.

The Async Context API would allow JavaScript to capture and use implicit call site information across transitions through the event loop. It allows developers to write async code much like synchronous code, even in cases where implicit information is required.

This API revolves around two fundamental constructs: AsyncContext.Variable and AsyncContext.Snapshot. These constructs allow developers to create, manipulate, and retrieve context variables in asynchronous code blocks, providing a mechanism to propagate a value through asynchronous code, such as a promise continuation or async callbacks.

The AsyncContext.Variable class

The AsyncContext.Variable class allows you to create a new variable that can be set and accessed within an asynchronous context. The run method sets a value for the variable for the duration of a function execution, while the get method retrieves the current value of the variable.

The AsyncContext.Snapshot class

The AsyncContext.Snapshot class, on the other hand, enables you to capture the current value of all AsyncContext.Variable instances at a given time. It’s like taking a “snapshot” of the state of all context variables, which can then be used to execute a function at a later time with those captured values.

More great articles from LogRocket:

These new constructs promise to dramatically simplify context management in asynchronous JavaScript. With the Async Context API, developers can maintain consistent access to the implicit information they need, regardless of how many asynchronous operations they perform.

Using AsyncContext

Let’s look at the namespace declaration of AsyncContext from the Async Context proposal:

namespace AsyncContext {
  class Variable<T> {
    constructor(options: AsyncVariableOptions<T>);

    get name(): string;

    run<R>(value: T, fn: (...args: any[])=> R, ...args: any[]): R;

    get(): T | undefined;

  interface AsyncVariableOptions<T> {
    name?: string;
    defaultValue?: T;

  class Snapshot {

    run<R>(fn: (...args: any[]) => R, ...args: any[]): R;

The AsyncContext namespace introduces a couple of essential classes and an interface:

  1. Variable: This class represents a context variable that can hold a value; the generic type <T> indicates that the variable can hold any data type. It also has a run() method that sets the variable’s value and executes a function, and its get() method returns the current value of the variable
  2. AsyncVariableOptions: This is an interface that defines the possible options you can pass to the Variable constructor. It can include a name for the variable and a default value
  3. Snapshot: This class captures the state of all AsyncContext.Variables at a specific moment. It has a run() method that executes a function with the values of the variables at the time the snapshot was taken

Understanding AsyncContext.Variable

Let’s dive deeper into how AsyncContext.Variable works. Suppose you have an asyncVar, an instance of AsyncContext.Variable. To set a value for asyncVar during the execution of a function, you’d use the run method. This method takes the following as its first two arguments:

  1. The value to set for asyncVar
  2. The function within whose execution asyncVar should hold that value

Here’s an example:

const asyncVar = new AsyncContext.Variable()
asyncVar.run('top', main)

In this code, asyncVar is set to the string “top” during the execution of the main function. This value can then be accessed within the main function using asyncVar.get(). Note that, through the AsyncContext API, AsyncContext.Variable instances are designed to maintain their value across synchronous and asynchronous operations, such as those queued using setTimeout.

Interestingly, AsyncContext.Variable runs can be nested. This means you can call the run method on asyncVar within a function already running with a set value for asyncVar. In this case, asyncVar takes on the new value for the duration of the inner function execution, then reverts to the previous value once the inner function has completed.

Making sense of AsyncContext.Snapshot

The AsyncContext.Snapshot class is used to capture the state of all AsyncContext.Variable instances at a certain point in time. This is done by calling the Snapshot constructor. The snapshot can later be used to run a function as if the captured values were still the current values of their respective variables. This is accomplished through the run method of the Snapshot instance.

Let’s see an example:

asyncVar.run('top', () => {
  const snapshotDuringTop = new AsyncContext.Snapshot()

  asyncVar.run('C', () => {
    snapshotDuringTop.run(() => {
      console.log(asyncVar.get()) // => 'top'

In this code, snapshotDuringTop produces a snapshot of the state of all AsyncContext.Variable instances at the time it was created. Later, during the run method of asyncVar, a function is executed using the run method of snapshotDuringTop.

Even though asyncVar is set to “C” at this point, within the snapshotDuringTop.run() execution, the value of asyncVar is still “top”, as that was its value when snapshotDuringTop was created.

The impact of the Async Context API

The proposed Async Context API holds significant potential for JavaScript developers, especially those working with server-side JavaScript environments like Node.js. This API would enable developers to handle context within asynchronous code more efficiently, offering functions to set, retrieve, and “freeze” context at specific points in the execution.

This could potentially free developers from the complexities of managing context within asynchronous operations and greatly enhance traceability and debugging, which could lead to better focus on writing effective and efficient code.

In more practical terms, if implemented, Async Context could significantly enhance a variety of use cases, such as:

  • Annotating logs with information related to an asynchronous call stack
  • Collecting performance information across logical asynchronous threads of control
  • Providing accurate insights into application performance

It could also improve the functionality of web APIs, such as Prioritized Task Scheduling, by maintaining context throughout request handling, even across multiple asynchronous operations.

Additionally, the Async Context API could help with improved error handling, allowing developers to trace exceptions more accurately to their original context, leading to faster resolution times. It could also provide more effective concurrency control by managing and maintaining context across asynchronous boundaries.

On a larger scale, the Async Context proposal represents an important advancement in the ongoing evolution of JavaScript, specifically for asynchronous programming. By addressing some of the unique challenges that asynchronous programming presents, it could provide a powerful, flexible solution. However, the proposal is still in its early stages, and its eventual impact will depend on its final implementation and adoption within the JavaScript community.


If implemented, the Async Context API would provide developers with more powerful tools to manage context within asynchronous code. The ability to propagate context in an asynchronous environment could be transformative, leading to more accurate tracing, debugging, and performance monitoring.

The future of JavaScript will likely see a continued emphasis on the effective handling of asynchronous operations, and proposals like Async Context represent necessary steps toward that future.

LogRocket: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket Dashboard Free Trial Banner

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

Try it for free.
Ebenezer Don Full-stack software engineer with a passion for building meaningful products that ease the lives of users.

Leave a Reply