console.table and console.time power tricks for real debugging
Published June 18, 2027
type Row = { id: number; status: string; durationMs: number };
export const logBatchResult = async (rows: Row[], errors: { field: string; message: string }[]) => {
console.table(rows, ["id", "status", "durationMs"]);
console.time("fetch-batch");
await new Promise((resolve) => setTimeout(resolve, 10));
console.timeLog("fetch-batch", "batch fetched");
console.timeEnd("fetch-batch");
console.group("validation errors");
errors.forEach((e) => console.error(e.field, e.message));
console.groupEnd();
console.count("retry");
};What
console.table() renders an array or object of records as an actual grid — sortable, scannable columns — instead of a collapsed {} tree you expand one entry at a time. console.time()/console.timeLog()/console.timeEnd() wrap a block with a matched label to report elapsed wall-clock time, and console.count() tracks how many times a labeled line has run — all built into every major console (browser and server runtime alike) with zero setup.
Why it matters
console.log on an array of 50 objects prints a collapsed tree nobody actually reads — you expand one entry, note the field you care about, collapse it, expand the next. console.table renders the same data as columns, so spotting the one row where status !== "ok" is a glance across a column, not an expand-fest. Timing gets the same upgrade: the classic approach is const start = performance.now(); …; console.log(performance.now() - start), which needs a variable, breaks if the block returns early, and stops being useful the moment you have more than one timer in flight. console.time/console.timeEnd replace that with two matched calls that handle the subtraction, the labeling, and — the less-known part — support multiple concurrent named timers without any manual bookkeeping.
How it works
The snippet chains four independent console tricks:
console.table(rows, ["id", "status", "durationMs"])— the second argument filters which columns render. Passing it matters for wide objects; without it, every property becomes a column and the table blows past a readable width.console.time("fetch-batch")starts a named timer;console.timeLog("fetch-batch", "batch fetched")prints an intermediate reading without stopping it — useful for "how far are we at this checkpoint" inside a longer operation;console.timeEnd("fetch-batch")prints the final elapsed time and removes the label.console.group("validation errors")/console.groupEnd()indents everything logged between them, so a batch of related lines (one per validation error, here) reads as a visually distinct block instead of blending into surrounding log noise.console.count("retry")prints a running count of how many times that labeled call has executed — handy for "how many times did this retry path actually run" without adding and mutating a counter variable yourself.
Gotchas
- Calling
console.time()again with a label that's already running does not restart the timer — it prints a"Timer 'label' already exists"warning and keeps the original start time. If you need a fresh reading inside a loop, callconsole.timeEnd()(or use a different label) before starting it again. console.table's column order and exact rendering differ by devtools/runtime — treat it as a debugging aid, not something to build automated tooling on top of by parsing its output.- None of these are part of ECMA-262 or a W3C Recommendation — the Console API is a de facto living standard (the WHATWG Console spec) that browsers, Node, Bun, and Deno all implement compatibly, but nothing in it is guaranteed byte-for-byte identical across runtimes.
console.count()'s counter is keyed by label globally per page/process, not per call site — two different functions callingconsole.count("retry")share the same counter, which is occasionally exactly what you want and occasionally a mislabeled surprise.
Related
performance.mark()/performance.measure()— the User Timing API; heavier-weight thanconsole.time, but the marks show up in the browser's Performance panel timeline, not just the console.debug(npm package) — namespaced, env-var-gated logging; a different axis (toggling verbosity) than the console tricks here (formatting and timing what you already decided to log).