logzly. Code Busting Hub

Debug Intermittent JS Race Conditions: 7 Steps

Read this article in clean Markdown format for LLMs and AI context.

Got a flaky bug that vanishes the moment you try to reproduce it? This guide shows exactly how to debug intermittent race conditions in JavaScript, turning a phantom issue into a repeatable test you can fix today.

Why intermittent race conditions slip by

The problem usually appears only in production because async functions finish in an unpredictable order, depending on network latency or CPU load. When the timing is just right, shared state ends up in an unexpected shape and the app crashes. Because the failure is non‑deterministic, standard logging often shows nothing useful.

1️⃣ Create a minimal reproducible case

Strip the surrounding code down to the smallest snippet that still triggers the bug. Move the async calls into a separate file and write a simple function that invokes them in the same order as the real app.

Result: you isolate the problem space and eliminate unrelated noise.

2️⃣ Add deterministic timing

Replace real network calls with mocked promises that resolve after a fixed timeout. Example:

function mockApi(delay) {
  return new Promise(resolve => setTimeout(() => resolve('ok'), delay));
}

Use Promise.resolve().then(() => …) together with setTimeout to force a specific race order.

Result: you control which promise settles first, making the race visible.

3️⃣ Instrument with logging

Insert a console.log before and after each async step, printing timestamps and the current state of shared variables:

console.log('🔹 start A', Date.now(), shared);
await mockApi(30);
console.log('🔹 end A', Date.now(), shared);

Result: the invisible race becomes a clear timeline you can scan.

4️⃣ Leverage Chrome DevTools

Open the Sources panel, set breakpoints on the async functions, and enable the “Async” call‑stack view. The detect and fix race conditions with Chrome DevTools feature highlights promise chains and shows which one resolves first.

Result: you get a visual map of execution flow without leaving the browser.

5️⃣ Run the test repeatedly

Wrap the reproducer in a loop that executes it dozens of times:

for (let i = 0; i < 50; i++) {
  runReproducer();
}

If the bug appears even once, the logs will reveal the offending promise order.

Result: a single flaky run becomes a statistically significant signal.

6️⃣ Isolate and refactor the culprit

When the logs point to a specific async call, refactor it to use await or a chained .then that enforces the desired sequence:

await mockApi(30);          // guarantees A finishes before B
await mockApi(10);

Result: the nondeterministic race is eliminated.

7️⃣ Write an automated regression test

Turn the whole scenario into a Jest test that asserts the correct order of operations:

test('async steps run in order', async () => {
  const order = await runReproducer();
  expect(order).toEqual(['A', 'B', 'C']);
});

Now the bug is caught automatically before any future changes.

Result: you protect the codebase with a safety net that never sleeps.

TL;DR

  1. Isolate the code.
  2. Mock promises with fixed delays.
  3. Log timestamps and state.
  4. Use Chrome DevTools’ async stack.
  5. Loop the test 50×.
  6. Refactor the offending async call.
  7. Add a Jest regression test.

Following this 7‑step process lets you debug intermittent race conditions without guesswork, giving you confidence that the bug is truly gone.

If this walkthrough helped you tame a stubborn race condition, subscribe for more practical JavaScript debugging tips, and share the guide with teammates battling similar bugs.

Reactions
Do you have any feedback or ideas on how we can improve this page?