Observation, hypothesis, experiment

Write expected and observed behaviour before choosing an experiment. Passing serially can support a shared-state hypothesis but does not establish the root cause alone. Timing, resource limits and other concurrency effects can produce the same observation.

The following intentionally constructed bug is deterministic and was run on Node.js v24.19.0. It is not a customer incident report and uses neither random delays nor an unverified failure rate.

The broken function

brokenRender writes to one sharedTitle variable and yields across a microtask boundary. Starting Alice and Bob together lets the second invocation overwrite the first invocation's value; both return Bob instead of their respective inputs.

The hypothesis is cross-invocation shared-state access. First record the parallel failure, then change only execution order to serial. That narrows the symptom; the decisive intervention removes shared state and returns each invocation's own parameter.

Run all three experiments

Save debugging-shared-state.mjs and run node debugging-shared-state.mjs using Node.js 24. It runs entirely in memory, with no requests, file changes or extra packages. Assertions separately check the expected broken output and corrected behaviour.

JavaScript
import assert from 'node:assert/strict';
let sharedTitle = '';
async function brokenRender(title) {
  sharedTitle = title;
  await Promise.resolve();
  return sharedTitle;
}
const expected = ['Alice', 'Bob'];
const parallel = await Promise.all(expected.map(brokenRender));
assert.deepEqual(parallel, ['Bob', 'Bob']);
console.log('parallel broken:', JSON.stringify(parallel));
const serial = [];
for (const title of expected) serial.push(await brokenRender(title));
assert.deepEqual(serial, expected);
console.log('serial broken:', JSON.stringify(serial));
async function fixedRender(title) {
  await Promise.resolve();
  return title; // Keep state local to each invocation.
}
const fixed = await Promise.all(expected.map(fixedRender));
assert.deepEqual(fixed, expected);
console.log('parallel fixed:', JSON.stringify(fixed));
console.log('PASS: deterministic reproduction and regression checks');

Experiment record and actual output

Parallel broken version: [Bob, Bob]. Serial broken version: [Alice, Bob]. Parallel fixed version: [Alice, Bob]. Serial execution was not treated as the permanent fix; the corrected function was checked under the same parallel condition.

The evidence is not merely a passing serial test: the code exposes shared writes, the collision is reproducible, and eliminating the shared read makes the parallel regression check pass. A real project needs corresponding experiments on its own data and execution model.

Terminal
parallel broken: ["Bob","Bob"]
serial broken: ["Alice","Bob"]
parallel fixed: ["Alice","Bob"]
PASS: deterministic reproduction and regression checks

What this test cannot prove

This is single-process JavaScript, not a test of worker threads, database isolation, HTTP requests or distributed races. Passing it cannot prove the entire application is free of concurrency bugs. Document the verified scope rather than saying only that it now works.

Do not add credentials or personal data to production logs. Start with safe request IDs, module versions and data shapes. Multiple simultaneous changes obscure the effective intervention; a changed result after a restart is not itself an explanation.

Apply the loop to your bug

Record expected and observed output, minimize the reproduction, write the hypothesis before the experiment, vary one factor, test counterexamples and retain a regression check. See the code-reading guide for impact analysis and the idempotency example for redelivery-related duplicates.

Source: Runnable debugging example

Source: Node.js assertion documentation

Source: Related guide: read before writing code

Source: Related guide: event idempotency