DEV Community

Cover image for The Race Condition Vanished the Instant I Added a console.log
Rohit Bhadani
Rohit Bhadani Subscriber

Posted on

The Race Condition Vanished the Instant I Added a console.log

The race condition disappeared the instant I added a console.log to narrow down where it was happening. Not "became harder to reproduce" — vanished completely, every single time, for as long as the log statement stayed in the code. Remove it, the bug came back within a few requests. This is the specific kind of debugging session that makes you doubt your own tools rather than your code, and for about twenty minutes I did.

Why a log statement can hide a race condition

console.log isn't free. It's a synchronous I/O operation in Node, and depending on the runtime and where stdout is being piped, it can take anywhere from microseconds to low milliseconds — small, but not zero, and crucially, not zero at exactly the point in the code where two async operations were racing to finish first.

async function processOrder(orderId) {
  const order = await fetchOrder(orderId);
  console.log('fetched', orderId); // this line changed the timing enough to hide the bug
  const inventory = await checkInventory(order.items);
  return reserveStock(order, inventory);
}
Enter fullscreen mode Exit fullscreen mode

Two concurrent calls to processOrder for the same orderId were both passing the inventory check before either one reserved stock — a classic check-then-act race. The log statement added just enough delay between the fetch and the inventory check that the two concurrent calls stopped overlapping in the exact window that mattered, without changing the actual logic at all.

Finding it without relying on accidental timing

Since anything I added to observe the bug risked changing its timing, I needed a way to force the race deliberately instead of hoping to catch it naturally:

// deliberately delay to force the interleaving, rather than hoping it happens
async function checkInventory(items) {
  if (process.env.FORCE_RACE_DELAY) await sleep(50);
  return actualInventoryCheck(items);
}
Enter fullscreen mode Exit fullscreen mode

Running two concurrent requests against the same order with that delay forced on reproduced the double-reservation every time, which turned a heisenbug into a bug I could reproduce on demand — the actual prerequisite for confidently fixing anything.

The real fix

Check-then-act races aren't fixed by adding delay, they're fixed by making the check-and-act atomic. A database-level constraint did the actual work here, rather than any application-level locking that would just move the race somewhere else:

ALTER TABLE inventory ADD CONSTRAINT stock_non_negative CHECK (quantity >= 0);
Enter fullscreen mode Exit fullscreen mode
UPDATE inventory SET quantity = quantity - 1
WHERE product_id = $1 AND quantity > 0
RETURNING quantity;
Enter fullscreen mode Exit fullscreen mode

The UPDATE ... WHERE quantity > 0 makes the check and the decrement a single atomic operation at the database level — there's no window between "check" and "act" for a second request to slip into, because there's no longer a separate check at all.

Why observability tools need to be trusted, not just present

The bigger lesson wasn't the specific race condition, it was realizing that adding instrumentation can change the behavior you're trying to observe, and the fix isn't to stop instrumenting, it's to instrument in ways that don't perturb timing — structured logging with async, non-blocking transport, or forcing the race deliberately rather than hoping default execution order surfaces it. We test concurrency-sensitive code paths against disposable Krova Cubes specifically because a dedicated, non-shared machine gives consistent enough timing that concurrency bugs reproduce reliably run to run, rather than depending on whatever unrelated load happens to be sharing the host that day. I run Krova, so weigh that as informed rather than neutral, but the core lesson — a race that vanishes when you look at it closer is usually a timing artifact of the looking, not a resolved bug — applies to debugging concurrency anywhere.

What I'd check first

If a bug disappears when you add logging or a debugger breakpoint, don't celebrate — that's a strong signal you have a race condition whose timing you just accidentally changed. Force the interleaving deliberately with an intentional delay behind a flag, reproduce it on demand, and fix the underlying check-then-act pattern at the level where it can actually be made atomic, usually the database or a proper lock, not just adding delay to make the symptoms go away.

Top comments (0)