Async Local Storage
- javascript
- node
- typescript
About
AsyncLocalStorage
is a Node class that allows you to create an asynchronous context that can be accessed anywhere throughout your application. It has a run
method that you can use to ensure the context stays local the function that it is called in.
For me, it was hard to understand without seeing an implementation so here’s my attempt to explain.
Framework implementations
A variety of server frameworks now use AsyncLocalStorage
to create convenient helper functions that feel like magic.
Next
For example, Next stores the request context within AsyncLocalStorage
to create functions like after
.
Here’s a snippet from their documentation showing how serverless platforms can integrate it:
import { AsyncLocalStorage } from "node:async_hooks";
const RequestContextStorage = new AsyncLocalStorage<NextRequestContextValue>();
// Define and inject the accessor that next.js will use
const RequestContext: NextRequestContext = {
get() {
return RequestContextStorage.getStore();
},
};
globalThis[Symbol.for("@next/request-context")] = RequestContext;
const handler = (req, res) => {
const contextValue = { waitUntil: YOUR_WAITUNTIL };
// Provide the value
return RequestContextStorage.run(contextValue, () => nextJsHandler(req, res));
};
SvelteKit
SvelteKit recently added the getRequestEvent
function that utilizes AsyncLocalStorage
under the hood as well.
This enables you to access things on the event
object like their optimized fetch
without having to pass it down from the load function. This will be particularly useful in Svelte’s new async components, since you could get the event within a deeply nested component and reuse the component anywhere without having to prop drill.
import { getRequestEvent } from "$app/server";
// no need to pass the `event` from `load`
const fn = () => {
const { fetch } = getRequestEvent();
};
Support
Support is great in popular server-side JavaScript runtimes with support in Node, Cloudflare, Deno, Bun, Vercel Edge Runtime and more.
At the current time, AsyncLocalStorage
is not supported in the browser or in web containers, so it will not work on platforms like StackBlitz.
Example
Here’s a bare bones example of how you can use AsyncLocalStorage
on a web server to store the current Request
and make it easy to access in deeply nested functions.
Without
Without AsyncLocalStorage
if you want to access something on the request, you’ll need to pass it down as an argument to whatever function needs it. This can quickly grow tedious in large application with deeply nested functions or components.
Here’s a server that does an async task and then returns the current pathname as a Response
. req
is passed down to getPathname
as an argument through each function between fetch
and getPathname
—in this case, handler
. In practice, there could be many more functions as intermediaries that need to pass the request without modifying it.
export default {
async fetch(req: Request) {
return handler(req); // pass to handler
},
};
const handler = async (req: Request) => {
await promise;
const pathname = getPathname(req); // pass to getPathname
return new Response(pathname);
};
const getPathname = (req: Request) => {
const url = new URL(req.url); // use the argument
return url.pathname;
};
At first glance, you might think you can assign the request to a global variable, and then use that variable where you need it.
let globalRequest: Request; // global variable to store
export default {
async fetch(req: Request) {
globalRequest = req; // set at the start of the request
return handler();
},
};
const handler = async () => {
await promise;
const pathname = getPathname();
return new Response(pathname);
};
const getPathname = () => {
const url = new URL(globalRequest.url); // access the global variable
return url.pathname;
};
There is a problem with this implementation: if multiple requests are being processed at the same time, the request will be overwritten with each new request. If one request is awaiting promise
while another comes in, it will return the pathname of the second request.
Implementation
AsyncLocalStorage
provides a per-request solution for this exact problem with it’s run
method.
import { AsyncLocalStorage } from "node:async_hooks";
const asyncLocalStorage = new AsyncLocalStorage<Request>();
// helper to get request anywhere within the scope of the `run`
const getRequest = () => {
const req = asyncLocalStorage.getStore();
if (!req) throw ReferenceError("Accessed outside of the run context.");
return req;
};
export default {
async fetch(req: Request) {
// run with the current request as the context
return asyncLocalStorage.run(req, () => {
return handler(); // run forwards the result
});
},
};
const handler = async () => {
await promise;
const pathname = getPathname();
return new Response(pathname);
};
const getPathname = () => {
const req = getRequest(); // no need to pass the request down in an argument
const url = new URL(req.url);
return url.pathname;
};
Using the req
as the first argument of run
makes it available in AsyncLocalStorage
within the scope of the function passed into the second argument—in this case, handler
.
You can see how this might be useful, now getPathname
can be called in any function or component within the scope of the run. If you have a component that uses the pathname and you need to use it in a variety of places, you don’t need to prop drill the request object to it, just call the component where you need it!