Loops are where console.log goes to multiply. One log per pass is fine for 8 passes and useless for 8,000. Here are the techniques I reach for, from "edit the code" to "don't touch it at all".
The loop we'll debug finds spikes in a list of readings:
const readings = [12, 15, 11, 40, 14, 13, 90, 12];
const spikes = [];
for (let i = 0; i < readings.length; i++) {
const delta = readings[i] - (readings[i - 1] ?? readings[i]);
if (delta > 20) spikes.push(i);
}
spikes; // [3, 6]
1. Log only what matters
Logging every pass buries the answer. Log the passes you care about:
for (let i = 0; i < readings.length; i++) {
const delta = readings[i] - (readings[i - 1] ?? readings[i]);
if (delta > 20) console.log({ i, delta });
if (delta > 20) spikes.push(i);
}
// { i: 3, delta: 29 }
// { i: 6, delta: 77 }
Logging an object ({ i, delta }) instead of two loose values means every line says what it is.
2. Collect rows, print a table once
When you do need every pass, collect them and print once at the end:
const trace = [];
for (let i = 0; i < readings.length; i++) {
const delta = readings[i] - (readings[i - 1] ?? readings[i]);
trace.push({ i, reading: readings[i], delta });
if (delta > 20) spikes.push(i);
}
console.table(trace);
One table with eight rows reads far better than eight scattered logs, and you can see the pattern (a big positive delta, then a big negative one) at a glance.
3. console.count for "how often?"
Sometimes you don't need the values, just how many times a branch ran:
if (delta > 20) console.count("spike");
// spike: 1
// spike: 2
4. Don't edit the code: logpoints
Every technique above changes the loop. That's a risk: you add a log, the bug moves, or the log ships. Debuggers solved this long ago with logpoints, a breakpoint that logs instead of pausing. Browser DevTools and most editors have them.
JSLab, a JavaScript & TypeScript scratchpad for macOS, has them too, without starting a debugger. Click the gutter beside a line, or press F9 with the caret on it, and that line is logged on every run. Its text doesn't change.
Put a logpoint on the const delta = … line and you get one row per pass, each tagged with that line, while the code stays exactly as you wrote it.
Here's a logpoint in a real JSLab tab: the dot beside line 5, and the 33.74 row tagged :5 that it produced. Line 5 itself is unchanged.
A few details from the docs worth knowing:
- A logpoint follows its line as you edit above or below it.
- Toggling one schedules a run, even if nothing else changed.
- ⇧⌘F9 clears every logpoint in the tab.
- Logpoints belong to one tab and aren't saved, so they can't leak into a file.
If you'd rather mark it in the code, a magic comment before the loop's opening brace logs each pass too:
for (const r of readings) /*?*/ {
// ...
}
Bonus: the loop that never ends
While you're editing a loop, you'll write while (i < 10) and forget i++. In JSLab, loop protection stops a loop after 2,000 iterations by default and the error names the line, so a slip doesn't hang the run. The limit is Loop Limit in Settings → Advanced.
Which one when
- Need the answer once: a conditional log.
- Need to see the pattern: collect rows,
console.table. - Don't want to touch the code: a logpoint.
JSLab launches Nov 15. Logpoints and loop protection are documented here: https://js-lab.app/docs/editor

Top comments (0)