Continuation-Local Storage (Hooked)
cls-hooked is a Node.js library that provides Continuation-Local Storage (CLS), a mechanism akin to thread-local storage but adapted for Node.js's asynchronous, callback-based execution model. It enables developers to maintain and implicitly pass contextual data across asynchronous operations without explicit parameter passing. The current stable version, 4.2.2, primarily targets Node.js versions `^4.7 || >=6.9 || >=7.3 || >=8.2.1`. Its key differentiator lies in its implementation, which leverages Node.js's internal, lower-level APIs: `AsyncWrap` for Node.js versions prior to v8 (an unofficial API), and the `async_hooks` API for Node.js v8.2.1 and newer (which is considered experimental). This approach distinguishes it from its predecessor, `node-continuation-local-storage`, which relied on the now-deprecated `async-listener`. The project's release cadence is largely influenced by the evolution and stability of these underlying Node.js APIs, aiming to provide a robust CLS implementation despite the experimental nature of its core dependencies.
Common errors
-
Error: A namespace called 'my-app-session' already exists. Use getNamespace(name) to retrieve a namespace.
cause Attempting to create a new continuation-local storage namespace using `createNamespace(name)` with a name that is already in use.fixIf a namespace with the desired name might already exist, retrieve it using `const myNamespace = cls.getNamespace('my-app-session');` instead of `createNamespace`. -
TypeError: Cannot read properties of undefined (reading 'set')
cause This error occurs when attempting to call `namespace.set()` or `namespace.get()` outside of an active CLS context, meaning the code is not running within a `namespace.run()` block or a function bound by `namespace.bind()`.fixEnsure that any code that needs to access or modify the CLS context is executed within a `namespace.run(function() { ... })` block, or by passing functions through `namespace.bind(func)` to propagate the context. -
Error: The 'this' context of 'AsyncResource' is not an instance of AsyncResource
cause This is a low-level error originating from Node.js's internal `async_hooks` or `AsyncWrap` API. It typically indicates an issue with how `cls-hooked` is interacting with the underlying asynchronous resource tracking, potentially due to an unsupported Node.js version, specific asynchronous patterns, or an internal bug in `cls-hooked`.fixFirst, verify that your Node.js version is explicitly supported by `cls-hooked`'s `engines` field. Second, ensure you are using the latest stable version of `cls-hooked` to benefit from any bug fixes related to `async_hooks` compatibility. If the problem persists, review complex or non-standard asynchronous flows in your application.
Warnings
- gotcha When running `cls-hooked` on Node.js versions prior to v8, the library relies on Node.js's internal `AsyncWrap` API, which is unofficial, undocumented, and not guaranteed to be stable or supported across Node.js minor versions. Use in production environments should consider this inherent risk.
- gotcha For Node.js versions v8.2.1 and above, `cls-hooked` utilizes the `async_hooks` API, which is labeled as `Experimental` by Node.js. This implies that the API may undergo breaking changes in future Node.js releases without a major version increment, potentially requiring updates to `cls-hooked` itself.
- breaking The underlying `async_hooks` API in Node.js has undergone breaking changes, for example, noted around Node.js v8.2.0, which necessitated specific updates within `cls-hooked` (e.g., v5.alpha.1 notes this). These changes in Node.js itself can break `cls-hooked` functionality if not using a compatible version.
- gotcha Prior to version 4.2.1, `cls-hooked` had known issues with memory leaks due to improper handling of asynchronous resources. These leaks could accumulate over time in long-running applications.
Install
-
npm install cls-hooked -
yarn add cls-hooked -
pnpm add cls-hooked
Imports
- createNamespace
import { createNamespace } from 'cls-hooked';const cls = require('cls-hooked'); const myNamespace = cls.createNamespace('my-app'); - getNamespace
import { getNamespace } from 'cls-hooked';const cls = require('cls-hooked'); const myNamespace = cls.getNamespace('my-app'); - Namespace.run
const session = cls.createNamespace('my-session'); session.run(() => { /* contextual code here */ });
Quickstart
const cls = require('cls-hooked');
const session = cls.createNamespace('my-app-session');
console.log('--- Starting CLS Example ---');
function performAsyncTask(callback) {
setTimeout(() => {
// Simulate some async operation that takes time
console.log(`[Async Task] Inside async operation. Context user: ${session.get('user') || 'N/A'}`);
callback();
}, 50);
}
session.run(() => {
const requestId = 'req-123';
session.set('requestId', requestId);
session.set('user', 'Alice');
console.log(`[Main Thread] Initial context set. Request ID: ${session.get('requestId')}, User: ${session.get('user')}`);
performAsyncTask(() => {
// Even after an async operation, the context should persist
console.log(`[Callback 1] After async task. Request ID: ${session.get('requestId')}, User: ${session.get('user')}`);
// Call another function that accesses the context
anotherFunction();
// Now, run another context nested within
session.run(() => {
session.set('user', 'Bob'); // This should be specific to the nested run
console.log(`[Nested Context] Inside nested run. Request ID: ${session.get('requestId')}, User: ${session.get('user')}`);
performAsyncTask(() => {
console.log(`[Nested Callback] After nested async task. Request ID: ${session.get('requestId')}, User: ${session.get('user')}`);
});
});
console.log(`[Callback 1 Cont.] After nested context block. User: ${session.get('user')} (should be Alice again)`);
});
});
function anotherFunction() {
console.log(`[Another Function] Accessing context. Request ID: ${session.get('requestId')}, User: ${session.get('user')}`);
}
// Outside any 'run' block, the context should not be available
setTimeout(() => {
console.log(`[Outside Context] After all operations. Request ID: ${session.get('requestId') || 'N/A'}, User: ${session.get('user') || 'N/A'}`);
}, 150);