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.loggoes to stdout whileconsole.errorandconsole.warngo 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.timeis 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.groupEndindent 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
| Method | Purpose | Stream |
|---|---|---|
console.log / info | General formatted output | stdout |
console.error / warn | Diagnostics and warnings | stderr |
console.dir(obj, opts) | Inspect with depth, colors, showHidden | stdout |
console.table(data, cols) | Tabular view of arrays/objects | stdout |
console.time / timeEnd / timeLog | Measure elapsed time by label | stdout |
console.trace(msg) | Message plus current stack trace | stderr |
console.assert(cond, msg) | Log only when condition is falsy | stderr |
console.count / countReset | Tally calls by label | stdout |
console.group / groupEnd | Indent nested output | stdout |
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.tablewhenever you are about to log an array of objects — it is far more readable thanJSON.stringify. - Send diagnostics to
console.error/console.warnso stdout stays clean for real program output and piping. - Use unique, descriptive labels for
console.timeandconsole.countto avoid collisions across modules. - Prefer
console.dir(obj, { depth: null })overconsole.logwhen you must see fully nested structures. - Keep
console.traceandconsole.assertfor development; strip or gate them behind a debug flag before shipping to production. - For anything performance-sensitive or long-lived, graduate from
console.timeto the Performance Hooks API and fromconsole.logto a structured logger.