· 4 min read
Print the path
Temporary print statements solve many bugs because they capture a rerunnable chronology with less setup than an interactive debugger.

Near midnight, I was attached to the right Node worker and paused inside the right function. The invoice was pending, the call stack looked ordinary, and stepping over charge produced one successful save. I had reproduced nothing.
Why was I single-stepping one worker to understand a race between two? The fact I needed was the order of events across processes, and pausing shows one process at one moment. I removed the breakpoint, added four temporary prints carrying an invoice id and a process id, then ran the stress test again. The terminal finally showed the shape of the bug:
settle invoice=8 worker=4121 phase=loaded state=pendingsettle invoice=8 worker=4197 phase=loaded state=pendingsettle invoice=8 worker=4121 phase=chargedsettle invoice=8 worker=4197 phase=chargedTwo workers loaded the same pending record before either one saved the result. The frame from worker 4121 looked reasonable because it omitted worker 4197, whose two lines supplied the dangerous interleaving.
A print is a narrow question#
Print debugging works best when the line asks one question you can answer on the next run. Did this branch execute for invoice 8? Which worker entered first? A dump of every local variable makes those answers harder to see.
Raw console.log("here") calls lose their meaning as soon as the output scrolls, so I write a disposable helper that prints one JSON line with a stable probe name, the id of the work, and the pid of the runtime that emitted it. The call sites then describe transitions rather than locations:
probe("loaded", invoice, { state: invoice.state });if (invoice.state === "pending") { await charge(invoice); probe("charged", invoice);}“Loaded” and “charged” survive refactoring better than “line 84” or “after second await.” More importantly, the two events can be compared across workers and across repeated runs.
The debugger keeps one excellent frame#
A breakpoint preserves one rich moment, while temporary prints preserve a cheap sequence across many moments. Use the debugger for the whole stack or an unfamiliar object graph. When the missing fact is order, the small trace usually reaches it faster.
Debugger authors have built this distinction into their tools. The GDB manual on tracepoints begins with programs whose real-time behavior can change when a debugger interrupts them, so a tracepoint records selected expressions while the target keeps running and lets you read the collected history after.
Pausing is especially awkward around timers, UI events, subprocesses, and lock contention. You can still debug those systems interactively, but each stop becomes part of the schedule you are trying to understand.
Printing perturbs that schedule too, since it performs I/O and allocates strings. I once put a log inside a tight retry loop, watched the flaky test turn green, and spent another hour congratulating the fix before removing the line brought the failure straight back.
Escalate when the question gets richer#
The debugger becomes the faster instrument once you know where to stop and need depth rather than chronology. A conditional breakpoint can catch the rare input without filling a terminal, and a watchpoint can answer who wrote a field when adding prints to every possible writer would be absurd.
Intermittent native failures deserve an even stronger tool. rr records one failing Linux execution, replays it deterministically, and supports reverse execution through GDB — the history that temporary prints approximate, plus the state you did not think to record beforehand.
I have not measured how often printing wins the first round, and memory corruption in C++ is a different sport from a TypeScript worker race. In ordinary application code, though, the setup cost matters. A probe that takes thirty seconds to place can disprove a theory before the debugger has attached to the child process you forgot existed.
After fixing the double charge with an atomic claim, I deleted the four settle probes and reran the stress test without them. It passed locally, then failed once on the slower CI worker because its timeout was too tight. I added one print back, this time around the clock.