Skip to content
JavaScript Basics: Add Optional Diagnostics Channels

JavaScript Basics: Add Optional Diagnostics Channels

October 24, 2026

A library can report useful events without deciding where to log them. Node.js diagnostics_channel connects a named publisher to optional subscribers in the same runtime. Use it for observation such as counters or debugging details, while keeping the application result independent of whether anyone listens.

These three independent examples were executed on Linux with Node.js v24.19.0. Use Node.js 24 for this tutorial; only that installed version was tested. They use built-in modules and assertions, with no package installation. They make no network requests and leave no data files behind. The output below is the observed output.

Prepare a scratch directory

You need Bash, Node.js on your PATH, a text editor, and the mktemp, rm, and rmdir utilities. Keep the same terminal open and save each named file as UTF-8 in the new directory. The .mjs extension selects an ES module.

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

1. Publish a named event and remove its subscriber

Save this as publish-diagnostic-event.mjs:

import assert from 'node:assert/strict';
import { channel, subscribe, unsubscribe } from 'node:diagnostics_channel';

const name = 'example-report:completed';
const completed = channel(name);
const received = [];
function observe(message, channelName) {
  received.push({ rows: message.rows, channelName });
}

subscribe(name, observe);
try {
  completed.publish({ rows: 3 });
  assert.deepEqual(received, [{ rows: 3, channelName: name }]);
  console.log(`${received[0].channelName}: ${received[0].rows} rows`);
} finally {
  unsubscribe(name, observe);
}
completed.publish({ rows: 99 });
assert.equal(received.length, 1);
assert.equal(completed.hasSubscribers, false);
console.log(`received after unsubscribe: ${received.length}`);

Run it:

node publish-diagnostic-event.mjs

Output:

example-report:completed: 3 rows
received after unsubscribe: 1

The assertion immediately after publish succeeds because handlers run synchronously. Keep the original function reference so unsubscribe() can remove it. The prefixed channel name reduces accidental collisions with other code; document both the name and message fields for consumers.

2. Avoid building diagnostics that nobody will read

Save this as prepare-diagnostics-on-demand.mjs:

import assert from 'node:assert/strict';
import { channel, subscribe, unsubscribe } from 'node:diagnostics_channel';

const name = 'example-report:preview';
const preview = channel(name);
let prepared = 0;
let observed;
function report(values) {
  if (!preview.hasSubscribers) return;
  prepared += 1;
  preview.publish({ sorted: [...values].sort((a, b) => a - b) });
}
function observe(message) {
  observed = message.sorted;
}

report([9, 2, 5]);
assert.equal(prepared, 0);
console.log(`preparations without listeners: ${prepared}`);
subscribe(name, observe);
try {
  report([9, 2, 5]);
  assert.equal(prepared, 1);
  assert.deepEqual(observed, [2, 5, 9]);
  console.log(`preparations with listener: ${prepared}`);
  console.log(`preview: ${observed.join(',')}`);
} finally {
  unsubscribe(name, observe);
}

Run it:

node prepare-diagnostics-on-demand.mjs

Output:

preparations without listeners: 0
preparations with listener: 1
preview: 2,5,9

The counter proves that copying and sorting are skipped when the channel has no listeners. Place the hasSubscribers check before constructing an expensive diagnostic payload. Only optional reporting belongs behind this condition; necessary validation or business work must still happen without subscribers.

3. Collect independent summaries without retaining every event

Save this as summarize-diagnostic-events.mjs:

import assert from 'node:assert/strict';
import { channel, subscribe, unsubscribe } from 'node:diagnostics_channel';

const name = 'example-report:batch';
const batches = channel(name);
const totals = { batches: 0, rows: 0 };
let failures = 0;
function countRows(message) {
  totals.batches += 1;
  totals.rows += message.rows;
}
function countFailures(message) {
  if (!message.ok) failures += 1;
}

subscribe(name, countRows);
subscribe(name, countFailures);
try {
  for (const message of [
    { rows: 4, ok: true },
    { rows: 0, ok: false },
    { rows: 6, ok: true },
  ]) {
    batches.publish(message);
  }
} finally {
  unsubscribe(name, countRows);
  unsubscribe(name, countFailures);
}
assert.deepEqual(totals, { batches: 3, rows: 10 });
assert.equal(failures, 1);
assert.equal(batches.hasSubscribers, false);
console.log(`batches: ${totals.batches}; rows: ${totals.rows}`);
console.log(`failures: ${failures}`);
console.log(`subscribers remain: ${batches.hasSubscribers}`);

Run it:

node summarize-diagnostic-events.mjs

Output:

batches: 3; rows: 10
failures: 1
subscribers remain: false

The producer sends one event while two observers compute separate summaries. Neither stores the messages or mutates them. The final assertions run after both observers have been removed, checking the summary and the cleanup together. In a real module, keep the agreed message shape small and stable.

Practical limits

This is an in-process diagnostics API, not a durable queue or a cross-process message bus. Synchronous subscribers add work to the publishing call. Node warns that a thrown subscriber error triggers uncaughtException, so handle recoverable observer failures inside the observer and keep handlers short. Do not publish credentials or entire private request objects merely because the channel is named for diagnostics. These examples use application-owned channels, not Node’s built-in tracing channels.

Clean up

In the same terminal, remove only the three named source files, return to your starting directory, and remove the now-empty scratch directory:

rm -- publish-diagnostic-event.mjs prepare-diagnostics-on-demand.mjs summarize-diagnostic-events.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on