10LOC
Chrome APIsintermediate

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, call console.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 calling console.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 than console.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).