Skip to content
Node.js nd debugging 5 min read

Debugging with console Methods

Most Node.js debugging starts and ends with console.log, but the console object exposes a much richer toolkit that can turn a noisy stream of output into something structured and immediately readable. Methods like console.table, console.dir, console.time, and console.trace let you inspect data shapes, measure durations, and capture call stacks without ever attaching a debugger. Knowing them well saves you from cluttering your code with hand-rolled formatting and helps you read program state at a glance.

Why console is more than log

The global console in Node.js is an instance of the Console class from the built-in node:console module. By default it writes formatted output to process.stdout and errors to process.stderr. Every method accepts util.format-style format specifiers (%s, %d, %o, %j), so you rarely need to manually stringify values.

const user = { id: 7, name: "Ada", roles: ["admin"] };

console.log("User %s has id %d", user.name, user.id);
console.log("Full object: %o", user);

Output:

User Ada has id 7
Full object: { id: 7, name: 'Ada', roles: [ 'admin' ] }

Tip: console.log goes to stdout while console.error and console.warn go to stderr. Keeping diagnostics on stderr means you can pipe real program output (stdout) elsewhere without it being polluted by logs.

Inspecting nested data with console.dir

console.log truncates deeply nested objects after two levels and renders them on a single line. console.dir gives you explicit control over inspection options, which is invaluable when you need to see the full shape of a complex structure.

const config = {
  server: { host: "localhost", port: 3000, tls: { enabled: true, cert: { path: "/etc/ssl" } } },
};

console.dir(config, { depth: null, colors: true });

Passing depth: null expands every level, while colors: true adds ANSI coloring in a terminal. You can also set showHidden: true to reveal non-enumerable properties.

Rendering arrays and objects as tables

console.table is one of the most underused tools in Node.js. Given an array of objects, it prints a neatly aligned grid with an index column, making list data far easier to scan than raw JSON.

const orders = [
  { id: 101, item: "Keyboard", qty: 2, price: 49.99 },
  { id: 102, item: "Mouse", qty: 1, price: 19.99 },
  { id: 103, item: "Monitor", qty: 1, price: 199.0 },
];

console.table(orders);

Output:

┌─────────┬─────┬────────────┬─────┬────────┐
│ (index) │ id  │ item       │ qty │ price  │
├─────────┼─────┼────────────┼─────┼────────┤
│ 0       │ 101 │ 'Keyboard' │ 2   │ 49.99  │
│ 1       │ 102 │ 'Mouse'    │ 1   │ 19.99  │
│ 2       │ 103 │ 'Monitor'  │ 1   │ 199    │
└─────────┴─────┴────────────┴─────┴────────┘

You can restrict the columns shown by passing an array of property names as the second argument:

console.table(orders, ["item", "qty"]);

Measuring elapsed time

For quick, ad-hoc performance checks, console.time and console.timeEnd measure the wall-clock duration between two points using a shared label. console.timeLog prints an intermediate reading without stopping the timer.

console.time("fetch-users");

const res = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await res.json();
console.timeLog("fetch-users", `received ${users.length} users`);

users.forEach((u) => u.name.toUpperCase());
console.timeEnd("fetch-users");

Output:

fetch-users: 213.402ms received 10 users
fetch-users: 214.118ms

Warning: console.time is great for one-off checks but it is not a substitute for real measurement. For repeatable, statistically meaningful timing use the Performance Hooks API, which avoids the formatting overhead of console.

Tracing the call stack

When you need to know how execution reached a certain point, console.trace prints a message together with the current stack trace to stderr — without throwing an error or halting the program.

function loadModule(name) {
  initialize(name);
}

function initialize(name) {
  console.trace("initialize called for %s", name);
}

loadModule("auth");

Output:

Trace: initialize called for auth
    at initialize (file:///app/index.js:6:11)
    at loadModule (file:///app/index.js:2:3)
    at file:///app/index.js:9:1

Assertions, counters, and grouping

Three more methods round out the toolkit:

  • console.assert(condition, ...message) writes an assertion failure message to stderr only when the condition is falsy. It never throws and never halts execution.
  • console.count(label) prints how many times it has been called with a given label — useful for spotting hot paths or unexpected re-renders.
  • console.group / console.groupEnd indent subsequent output, letting you visually nest related logs.
function process(value) {
  console.count("process");
  console.assert(value >= 0, "value must be non-negative, got %d", value);
}

console.group("Batch");
process(5);
process(-1);
process(10);
console.groupEnd();

Output:

Batch
  process: 1
  process: 2
  Assertion failed: value must be non-negative, got -1
  process: 3

Use console.countReset(label) to zero a counter between runs.

Method reference

MethodPurposeStream
console.log / infoGeneral formatted outputstdout
console.error / warnDiagnostics and warningsstderr
console.dir(obj, opts)Inspect with depth, colors, showHiddenstdout
console.table(data, cols)Tabular view of arrays/objectsstdout
console.time / timeEnd / timeLogMeasure elapsed time by labelstdout
console.trace(msg)Message plus current stack tracestderr
console.assert(cond, msg)Log only when condition is falsystderr
console.count / countResetTally calls by labelstdout
console.group / groupEndIndent nested outputstdout

In CommonJS modules the same global console is available, and you can also build a dedicated instance with const { Console } = require("node:console") (or import { Console } from "node:console" in ESM) to direct output to custom streams such as a log file.

Best Practices

  • Reach for console.table whenever you are about to log an array of objects — it is far more readable than JSON.stringify.
  • Send diagnostics to console.error/console.warn so stdout stays clean for real program output and piping.
  • Use unique, descriptive labels for console.time and console.count to avoid collisions across modules.
  • Prefer console.dir(obj, { depth: null }) over console.log when you must see fully nested structures.
  • Keep console.trace and console.assert for development; strip or gate them behind a debug flag before shipping to production.
  • For anything performance-sensitive or long-lived, graduate from console.time to the Performance Hooks API and from console.log to a structured logger.
Last updated June 14, 2026
Was this helpful?