jsguides

diagnostics_channel

diagnostics_channel.channel(name)

The node:diagnostics_channel module provides a lightweight named pub/sub mechanism for publishing structured diagnostic data. Library authors create channels by name and publish arbitrary message objects; observability tools, APM agents, and other consumers subscribe to those channels and react. The module has been stable since Node.js v19.2.0 (and v18.13.0), and the underlying source lives in lib/diagnostics_channel.js.

It is imported explicitly in ESM and required in CommonJS. The node: prefix is the modern convention introduced in Node 14.13.1; the older unprefixed form require('diagnostics_channel') still works for backward compatibility but is no longer recommended in new code. Both forms return the same module object, so the rest of this page treats them as interchangeable:

import diagnostics_channel from 'node:diagnostics_channel';

In CommonJS the require call returns the same module object, and most codebases alias it to a shorter local name like dc to keep call sites compact. In ESM the default export is the module object itself, so you can call diagnostics_channel.channel('app:foo') directly without an extra wrapper:

const diagnostics_channel = require('node:diagnostics_channel');

The rest of this page covers the module-level API, the Channel class, the higher-level TracingChannel helper, the bridge to AsyncLocalStorage, and the built-in channels Node.js core itself publishes.

Module-level API

The module exposes four top-level helpers and one factory. The factory is the entry point you will use in library code; the helpers are convenience wrappers that work when you only have a channel name on hand.

diagnostics_channel.hasSubscribers(name)

Returns true if any subscriber is currently registered for the given channel name. Use it to skip message construction on a hot path before calling publish.

const dc = require('node:diagnostics_channel');

dc.hasSubscribers('app:request'); // false (no subscribers yet)

diagnostics_channel.channel(name)

Returns the cached Channel object for the given name. The cache is internal, so the function is cheap to call repeatedly. The official guidance is to create the channel once at module top level and export the reference, instead of calling channel(name) inside hot loops.

diagnostics_channel.subscribe(name, onMessage)

Added in v18.7.0. Resolves the channel for name and calls channel.subscribe(onMessage) on it. Errors thrown in the handler propagate to process.on('uncaughtException') — wrap the handler in try/catch if you do not want that.

const dc = require('node:diagnostics_channel');

function onMessage(message) {
  // ...handle the message
}

dc.subscribe('app:request', onMessage);

diagnostics_channel.unsubscribe(name, onMessage)

Added in v18.7.0. Resolves the channel for name and removes the previously registered onMessage handler. Returns true if the handler was found and removed, false otherwise. Module-level unsubscribe is the symmetric counterpart to subscribe and follows the same sugar-over-Channel pattern: it looks up the channel by name and calls channel.unsubscribe(onMessage) on it. The function never throws when the channel has no subscribers, so it is safe to call from cleanup paths:

dc.unsubscribe('app:request', onMessage); // true

The Channel class

Channel objects are constructed only via diagnostics_channel.channel(name). Calling new Channel(name) directly is not supported.

channel.name

Read-only. The string or symbol that identifies the channel.

channel.hasSubscribers

Read-only boolean. The local equivalent of diagnostics_channel.hasSubscribers(name). It is cheaper on a hot path because it skips the name lookup.

channel.publish(message)

Calls every subscribed handler synchronously, in registration order, in the call stack of publish(). There is no buffering and no microtask hop, so a slow subscriber blocks whatever called publish.

ParameterTypeDescription
messageunknownThe payload to send to subscribers. Plain objects are the most common choice.

Returns: void

const dc = require('node:diagnostics_channel');
const ch = dc.channel('app:request');

ch.subscribe((message, name) => {
  console.log(name, message);
});

ch.publish({ url: '/users', method: 'GET' });
// app:request { url: '/users', method: 'GET' }

When no subscribers are present, publish still pays a hasSubscribers check, so on a hot path guard the call site and skip the message construction:

const ch = dc.channel('app:hot-path');

if (ch.hasSubscribers) {
  ch.publish({ ts: Date.now(), payload: heavyBuild() });
}

channel.subscribe(onMessage)

Registers a handler that receives (message, name) for every published event. A documentation-only deprecation was added in v18.7.0 and revoked in v24.8.0 / v22.20.0, so this is still the supported API.

ch.subscribe((message) => {
  console.log('got', message);
});

channel.unsubscribe(onMessage)

Removes a previously registered handler. Returns true if the handler was found, false otherwise. Since v17.1.0 this works even when no subscribers are present (it returns false cleanly in that case).

TracingChannel

diagnostics_channel.tracingChannel(nameOrChannels) returns a TracingChannel that bundles five internal Channel objects. When you pass a string, the channels are named:

  • tracing:${name}:start
  • tracing:${name}:end
  • tracing:${name}:asyncStart
  • tracing:${name}:asyncEnd
  • tracing:${name}:error

Alternatively, pass an object of pre-existing Channel instances (each property is a Channel) to take full control over channel identity.

Create one TracingChannel at the top of a file and reuse it. Events fire only if a subscriber is registered before the trace begins. Subscribing after traceSync, tracePromise, or traceCallback starts will not retroactively deliver events from the in-flight trace.

tracingChannel.subscribe(subscribers) and tracingChannel.unsubscribe(subscribers)

Subscribe to or unsubscribe from a subset of the five internal channels. subscribers is an object whose keys are start, end, asyncStart, asyncEnd, and error, with handler functions as values. unsubscribe returns true if all handlers were removed, false if any were missing.

const dc = require('node:diagnostics_channel');
const traces = dc.tracingChannel('app:compute');

traces.subscribe({
  start(msg)  { console.log('start', msg); },
  end(msg)    { console.log('end', msg.result); },
  error(msg)  { console.error('error', msg.error.message); },
});

tracingChannel.traceSync(fn, context?, thisArg?, …args)

Wraps a synchronous function. Emits start with the context, runs fn inside the bound stores context, then emits end. Emits error and rethrows if fn throws. Returns the return value of fn.

traces.traceSync(() => 1 + 1, { requestId: 'abc' });
// start { requestId: 'abc' }
// end 2

tracingChannel.tracePromise(fn, context?, thisArg?, …args)

Wraps a function that returns a Promise. Emits start, then end when the synchronous portion returns, then asyncStart and asyncEnd when the continuation runs. Emits error and rethrows on rejection. Returns the chained promise.

const traces = dc.tracingChannel('app:fetch');

traces.subscribe({
  start(m)      { console.log('start', m); },
  asyncStart(m) { console.log('asyncStart', m); },
  asyncEnd(m)   { console.log('asyncEnd', m.result); },
  error(m)      { console.error('error', m.error.message); },
});

await traces.tracePromise(
  () => fetch('https://example.com').then((r) => r.status),
  { url: 'https://example.com' }
);

tracingChannel.traceCallback(fn, position?, context?, thisArg?, …args)

Wraps a Node-style callback function. position is the zero-indexed argument slot of the expected callback and defaults to -1 (the last argument). Emits start and end around the synchronous portion, asyncStart and asyncEnd around the callback, and error if fn throws or the callback receives an error as its first argument.

const fs = require('node:fs');
const traces = dc.tracingChannel('fs:readFile');

traces.subscribe({
  start(m)    { console.log('start', m.path); },
  asyncEnd(m) { console.log('asyncEnd bytes', m.result.length); },
});

traces.traceCallback(fs.readFile, -1, { path: 'package.json' });

Bridging AsyncLocalStorage

channel.bindStore(store, transform?) ties an AsyncLocalStorage instance to a channel. When channel.runStores(context, fn, ...) is called, the context flows through transform (or is used directly when no transform is supplied) and becomes the store’s value for the duration of fn. The published message arrives at subscribers inside that store scope, so a subscriber that reads store.getStore() sees the same context the traced function will see.

const dc = require('node:diagnostics_channel');
const { AsyncLocalStorage } = require('node:async_hooks');

const requestStore = new AsyncLocalStorage();
const ch = dc.channel('app:request');

ch.bindStore(requestStore, (msg) => ({ id: msg.id }));

ch.runStores({ id: 'r-1' }, () => {
  console.log(requestStore.getStore()); // { id: 'r-1' }
  ch.publish({ id: 'r-1', url: '/x' });
});

channel.bindStore(store[, transform])

ParameterTypeDescription
storeAsyncLocalStorageThe store to bind.
transform(context: T) => S (optional)A function that maps the run-stores context to the store value.

Re-binding replaces the previous transform.

channel.unbindStore(store)

Removes a previously bound store. Returns true if a store was unbound, false otherwise.

channel.runStores(context, fn[, thisArg[, ...args]]) and channel.runStores(context, fn, thisArg?, ...args)

Enters the bound stores’ context, runs fn (optionally bound to thisArg with extra args), and publishes the message inside that scope. The parent store value is reachable from inside transform via store.getStore(); that is the hook used to link child spans to a parent. A subscriber that reads the store during publish sees the same context the traced function will see, which is how a tracing layer attaches parentId to nested traces:

ch.bindStore(requestStore, (msg) => ({
  id: msg.id,
  parentId: requestStore.getStore()?.id,
}));

ch.subscribe((msg) => {
  console.log(msg.id, 'child of', msg.parentId);
});

ch.runStores({ id: 'r-2' }, () => {
  ch.runStores({ id: 'r-3' }, () => {});
  // logs: r-3 child of r-2
});

Built-in channels

Node.js core publishes a small set of channels from its built-in modules. Subscribe to these to observe HTTP traffic, DNS lookups, net sockets, and require() calls without modifying the originating code.

ChannelTriggered byPayload fields
http.client.request.startOutbound http/https request createdrequest
http.client.request.errorOutbound request error before headersrequest, error
http.client.response.finishOutbound response fully consumedrequest, response
http.server.request.startIncoming http.Server request receivedrequest, response
http.server.request.errorIncoming request errorrequest, response, error
http.server.response.finishIncoming response fully sentrequest, response
net.client.socketnet/tls client socket createdsocket
net.server.socketnet/tls server socket createdsocket
dns.lookupdns.lookup() callhostname, family, addresses or error
module.require.startrequire() invocation beginsrequest
module.require.endrequire() resolvesrequest, module
module.require.cacherequire() returned a cached modulerequest, module

The exact payload shapes evolve across Node.js versions, so treat the Node.js documentation as the source of truth when integrating against a specific release.

const dc = require('node:diagnostics_channel');

dc.subscribe('http.client.request.start', (message) => {
  console.log('outgoing', message.request.method, message.request.path);
});

dc.subscribe('http.client.response.finish', (message) => {
  console.log('finished', message.response.statusCode);
});

require('node:http').get('https://example.com');

Common mistakes

Subscriber errors propagate to process.on('uncaughtException'). A throwing handler is treated as an uncaught error in the process. Catch inside the handler if that is not what you want.

publish runs handlers in the call stack. Doing I/O inside a subscriber on a hot path will slow down whatever called publish. Offload heavy work to a queue or a worker.

For TracingChannel, the subscriber must be in place before the trace starts. Late subscribers miss the events from the in-flight call.

Choose channel names that include the publishing module, for example my-lib:db-query rather than db-query. The Node.js docs recommend the namespaced form to avoid collisions across libraries.

diagnostics_channel.boundedChannel(nameOrChannels) (added in v26.1.0, experimental) is a slimmer TracingChannel with only start and end channels. Use it for short, fully synchronous operations where the full five-event shape is unnecessary.

See also

  • events — the older EventEmitter pub/sub primitive; useful contrast for the Channel design (no max-listeners warning, separate channel objects, no 'error' event semantics)
  • http — context for the built-in http.client.* and http.server.* channels
  • processprocess.on('uncaughtException') is what catches errors thrown in subscribers