What is continuation-local-storage?
The continuation-local-storage (CLS) npm package provides a way to maintain context across asynchronous calls in Node.js. It allows you to create a namespace to store data that is accessible across different asynchronous operations, which is useful for tasks like logging, error tracking, and request tracing.
What are continuation-local-storage's main functionalities?
Creating a Namespace
This feature allows you to create a namespace where you can store and retrieve data across asynchronous calls.
const cls = require('continuation-local-storage');
const namespace = cls.createNamespace('myNamespace');
Setting and Getting Data
Within a namespace, you can set and get data that will be available throughout the asynchronous call chain.
namespace.run(() => {
namespace.set('key', 'value');
console.log(namespace.get('key')); // Outputs: value
});
Using with Asynchronous Functions
This feature demonstrates how to use CLS with asynchronous functions, ensuring that the context is maintained across asynchronous boundaries.
const asyncFunction = async () => {
namespace.set('key', 'value');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(namespace.get('key')); // Outputs: value
};
namespace.run(() => {
asyncFunction();
});
Other packages similar to continuation-local-storage
async_hooks
The async_hooks module is a core Node.js module that provides an API to track asynchronous resources. It is more low-level compared to continuation-local-storage and offers more control but requires more boilerplate code.
cls-hooked
cls-hooked is a modern alternative to continuation-local-storage that uses the async_hooks module under the hood. It provides a similar API to CLS but with better performance and support for newer Node.js versions.
zone.js
zone.js is a library that provides execution contexts (zones) that persist across asynchronous operations. It is more commonly used in Angular applications but can be used in Node.js as well. It offers more features but is also more complex.
Continuation-Local Storage
Stability: 1 - Experimental
Continuation-local storage provides a mechanism similar to thread-local storage
in threaded programming, with closures wrapped around portions of a
continuation chain taking the place of mutable cells bound to thread data
structures. Contexts are created on namespaces and can be be nested.
Every namespace is created with a default context. The currently active
context on a namespace is available via namespace.active
.
var cls = require('contination-local-storage');
var writer = cls.createNamespace('writer');
writer.set('value', 0);
function requestHandler() {
writer.run(function (outer) {
writer.set('value', 1);
process.nextTick(function () {
writer.run(function (inner) {
writer.set('value', 2);
});
});
});
setTimeout(function () {
console.log(writer.get('value'));
}, 1000);
}
cls.createNamespace(name)
Each application that wants to use continuation-local attributes should create
its own namespace. Reading from (or, more significantly, writing to) namespaces
that don't belong to you should be considered a faux pas.
cls.getNamespace(name)
Look up an existing namespace.
process.namespaces
- return: dictionary of {Namespace} objects
Set of available namespaces.
Class: Namespace
Application-specific namespaces provide access to continuation-local
attributes, and may have specialized behavior on a per-namespace basis (custom
nesting behavior). Once the execution of a handler chain begins, creating new
contexts and changing local values should be considered mutating the value of a
given attribute on that particular continuation chain.
namespace.active
Returns the currently active context on a namespace.
namespace.set(key, value)
Set a value on the current continuation context. Shorthand for
namespace.active.set(key, value)
.
Returns the value.
namespace.get(key)
Look up a value on the current continuation context. Recursively searches from
the innermost to outermost nested continuation context for a value associated
with a given key.
namespace.run(continuation)
Create a new scope to which attributes can be bound or mutated. Run the
continuation in this new scope (passing in the new context into the
continuation). It also returns the context that just exited in case you want
to read data from it.
namespace.bind(callback, [context])
Bind a function to the specified continuation context. Works analagously to
Function.bind()
or domain.bind()
. If context is omitted, it will default to
the currently active context in the namespace.
context
A context is just a plain object that inherits from it's parent context.
Rationale
The domains mechanism is a useful tool for adding context to errors
raised in asynchronous call chains (or, if you like living dangerously /
tempting the wrath of @isaacs, to recover from errors without restarting
your services). It also almost serves the purposes of developers
who want to annotate async call chains with metadata or extra state
(examples: logging, tracing / profiling, generic instrumentation),
but due to the needs of error-handling, it doesn't quite generalize
enough to be truly useful in this regard. There are modules that allow
developers to do similar things when they have full control over their
stacks (CrabDude/trycatch and
Gozala/reducers, among many
others), but none of these modules are a good fit for developers writing
tooling meant to be dropped transparently into user code.
See also joyent/node#3733.
Here is a sketch at what the user-visible API might look like. My
original attempt at this used a slightly modified version of the domains
API with some special-purpose logic for dealing with nested contexts,
but allowing multiple distinct namespaces is actually simpler and trades
memory for execution time. It also makes it possible to special-case
behavior for specific namespaces (i.e. my hope would be that domains
would just become a specialized namespace, and _tickDomainCallback
and _nextDomainTick
would be all that would be required to deal with
namespaces), although that isn't included here.
Here's an example of how the API might be used:
var cls = require('continuation-local-storage');
var tracer = cls.createNamespace('tracer');
function Trace(harvester) {
this.harvester = harvester;
}
Trace.prototype.runHandler = function (callback) {
var trace = tracer.run(callback);
this.harvester.emit('finished', trace.transaction);
};
Trace.prototype.annotateState = function (name, value) {
tracer.set(name, value);
};