Node.js console.dir(): Practical Object Inspection for Real Debugging

The moment my debugging slows to a crawl is usually the same moment my logs turn into lies. I’ll print an object, see [Object], a truncated array, a missing property, or a pretty string that hides the shape I actually need. Then I start guessing: “Is this field undefined, non-enumerable, behind a getter, or nested deeper than the logger shows?” That guessing is where bugs get expensive.

When I want to stop guessing and start seeing, I reach for console.dir().

console.dir() is a focused tool: it prints an inspectable view of an object’s properties (including nested children), and in Node.js it lets you control the inspection behavior with an options object. That control matters in real systems—API payloads, ORM models, Error objects, complex class instances, Buffers, streams, and data structures with symbols or non-enumerable properties.

If you want a reliable way to answer “What is this thing, right now?”—without rewriting half your code—console.dir() earns a permanent spot in your debugging kit.

What console.dir() really does in Node.js

In Node.js, console.dir(value) prints a representation of value that’s designed for inspection, not for “pretty app output.” Under the hood, Node’s console inspection is closely aligned with util.inspect() behavior. Practically, you can think of it like this:

  • console.dir() is object-first. You give it a value and it prints the properties.
  • It supports inspection options so you can control depth, colors, hidden properties, and more.
  • It’s ideal when console.log() gives you a result that’s too shallow, too formatted, or too easy to misread.

A quick baseline example:

// save as dir-basic.js

const customer = {

id: "cus_20491",

plan: "pro",

billing: {

interval: "monthly",

nextChargeAt: new Date("2026-02-15T00:00:00Z"),

},

};

console.dir(customer);

That prints the object with nested structure up to a default depth (the depth matters; I’ll show you how to control it soon).

One important mental model: console.dir() is not “better logging.” It’s “better inspection.” If you’re trying to produce stable, machine-parseable logs for production, I recommend structured logging (more on that later). But for developer debugging and one-off visibility, console.dir() is exactly the right tool.

The signature and the options that actually matter

You’ll often see the simple signature:

  • console.dir(object)

In Node.js, you can also pass an options object:

  • console.dir(object, options)

Those options are the difference between “I sort of see it” and “I can prove what’s happening.” Here are the knobs I use most often:

  • depth: How many nested levels to print.
  • colors: Whether to use ANSI colors (useful in terminals).
  • showHidden: Show non-enumerable properties and symbol-keyed properties.
  • maxArrayLength: Avoid dumping massive arrays.
  • maxStringLength: Avoid printing megabytes of text.
  • compact: Control line wrapping and density.
  • sorted: Print keys in sorted order (nice for diffs).
  • getters: Whether to evaluate getters during inspection (dangerous if getters have side effects).
  • customInspect: Whether to respect custom inspection methods on objects (great until you need the raw truth).

Here’s the first option I reach for: depth.

// save as dir-depth.js

const response = {

status: 200,

data: {

user: {

profile: {

preferences: {

theme: "dark",

notifications: { email: true, sms: false },

},

},

},

},

};

// Default depth may truncate nested levels.

console.dir(response);

// Show more levels.

console.dir(response, { depth: 6 });

// Show everything (be careful with huge objects).

console.dir(response, { depth: null });

If you’ve ever stared at an object wondering why a nested field “isn’t there,” it’s often there—just beyond your current inspection depth.

Next: colors, which is pure ergonomics.

// save as dir-colors.js

const event = {

type: "invoice.paid",

createdAt: new Date(),

metadata: { region: "us-east", attempt: 1 },

};

console.dir(event, { colors: true, depth: 4 });

I enable colors when I’m in a terminal and reading lots of logs. I disable them when logs are captured to files or CI output, where escape codes are noise.

console.dir() vs console.log(): where the difference shows up

A lot of developers try console.log(someObject) and feel like that should be enough. Sometimes it is. But I’ve repeatedly hit cases where console.log() encourages shallow thinking:

  • console.log() often gets used with string formatting, which can hide object structure.
  • console.log() output can get noisy when you mix templates and values.
  • In complex cases (classes, custom inspection), the printed output can be “nice” but not “true.”

Here’s an example that shows how formatting can distract you from the data shape:

// save as log-vs-dir.js

const job = {

id: "job_901",

state: "running",

worker: { pid: process.pid, host: "runner-03" },

};

console.log("job=%s", job); // Often not what you want: %s coerces to string

console.log("job=%j", job); // JSON stringifies, loses symbols, functions, non-enumerables

console.log("job=", job); // Usually okay, but defaults can still truncate

console.dir(job, { depth: 5 }); // Intent is explicit: inspect this object

Two key takeaways:

  • %j is tempting, but JSON.stringify has blind spots (symbols, BigInt, circular refs, custom types).
  • console.dir() makes your intent explicit and gives you control.

When I’m debugging, I want the log line to be unambiguous. console.dir() is me saying: “Ignore my formatting, just show me the object.”

Inspecting the tricky stuff: classes, prototypes, symbols, and non-enumerables

Real Node.js apps don’t live in plain objects. You’ll deal with:

  • class instances (with prototype methods)
  • private-ish state tucked away in non-enumerable properties
  • symbol-keyed properties used by frameworks
  • objects that redefine how they print

This is where showHidden and customInspect matter.

Seeing non-enumerables and symbols with showHidden

If a library stores data in a non-enumerable property, a shallow print makes it look like the field doesn’t exist.

// save as dir-hidden.js

const internalId = Symbol("internalId");

const session = {};

Object.defineProperty(session, "token", {

value: "sess_9f3a2c",

enumerable: false,

});

session[internalId] = "int_001";

console.log("Object.keys:", Object.keys(session));

console.dir(session); // token may not show

console.dir(session, { showHidden: true }); // shows non-enumerables and symbols

When you’re debugging authentication/session state, that showHidden: true toggle can save a full afternoon.

Bypassing “helpful” custom formatting with customInspect: false

Some objects implement custom inspection to print a friendly summary. That’s great until the summary hides the field you care about.

// save as dir-custom-inspect.js

import util from "node:util";

class Payment {

constructor(amountCents, currency) {

this.amountCents = amountCents;

this.currency = currency;

this._rawGatewayPayload = { authCode: "A7K2", risk: { score: 42 } };

}

[util.inspect.custom]() {

return Payment(${this.currency} ${(this.amountCents / 100).toFixed(2)});

}

}

const payment = new Payment(1299, "USD");

console.log(payment); // prints the friendly summary

console.dir(payment); // may also use custom inspect

// Force raw inspection

console.dir(payment, { customInspect: false, depth: 6 });

If you’re investigating “why did the gateway reject this payment,” you want _rawGatewayPayload, not a cute one-liner.

Debugging without self-sabotage: getters, circular refs, huge payloads, and PII

console.dir() is powerful enough to get you into trouble. Here are the mistakes I see (and I’ve made) most often.

1) Accidentally triggering getters

Getters can compute values, fetch data, or throw errors. Some are harmless; some have side effects.

If you suspect getters are involved, don’t casually print with aggressive inspection settings. Use options that avoid evaluating getters (and be conservative about depth: null).

Practical pattern: inspect the object shape first, then inspect specific fields deliberately.

// save as dir-getter-safety.js

class UserRecord {

constructor(email) {

this.email = email;

}

get profile() {

// Imagine this triggers lazy loading or throws if disconnected.

throw new Error("profile getter executed during debug print");

}

}

const u = new UserRecord("[email protected]");

// Safer: keep depth small while you learn what you‘re holding.

console.dir(u, { depth: 1 });

// If you then want profile, access it explicitly in a try/catch.

try {

console.dir(u.profile, { depth: 4 });

} catch (err) {

console.dir(err);

}

2) Circular references

Complex request/response objects, especially around servers, can contain cycles. JSON.stringify will throw; inspection tools generally handle cycles, but the output can get large.

If you see your terminal flood, clamp it:

  • set depth to a sane number (3–6 is usually enough)
  • set maxArrayLength
  • set maxStringLength

3) Logging huge objects (performance + readability)

Printing a 50k-element array or a 10MB string isn’t “more visibility,” it’s a denial-of-service against your own attention.

I like defaults like these when debugging large payloads:

// save as dir-large-payloads.js

const payload = {

ids: Array.from({ length: 20000 }, (, i) => id${i}),

rawHtml: "<div>" + "x".repeat(200000) + "</div>",

};

console.dir(payload, {

depth: 4,

maxArrayLength: 20,

maxStringLength: 200,

compact: false,

});

On most machines, dumping massive structures can push your debug loop from “instant feedback” to “seconds of waiting.” In typical dev setups, I’ve seen object printing swing from roughly 10–30ms for small objects to 200–800ms when you print large nested payloads with high depth. You’ll feel that lag.

4) Accidentally printing secrets and personal data

This is the one I treat as a habit, not a warning.

If you call console.dir(req) on an HTTP request, you may dump:

  • cookies
  • auth headers
  • session identifiers
  • user email/phone

My rule: when the object could contain secrets, log a filtered view.

// save as dir-redaction.js

function redactHeaders(headers) {

const copy = { ...headers };

for (const key of Object.keys(copy)) {

if (key.toLowerCase() === "authorization") copy[key] = "[REDACTED]";

if (key.toLowerCase() === "cookie") copy[key] = "[REDACTED]";

}

return copy;

}

const req = {

method: "POST",

url: "/api/payments",

headers: {

authorization: "Bearer secret-token",

cookie: "sid=sess_abc",

"content-type": "application/json",

},

body: { amountCents: 1299, currency: "USD" },

};

console.dir(

{ ...req, headers: redactHeaders(req.headers) },

{ depth: 6, colors: true }

);

You still get visibility, but you’re not training yourself to leak secrets into logs.

Real-world recipes: where console.dir() saves time

Here are the cases where I reliably reach for console.dir() in production-grade Node.js codebases.

Recipe 1: Inspect an Error object the way you actually need

Errors often hide important fields (like cause, custom properties, nested validation data). Don’t just print err.message and move on.

// save as dir-error.js

function doWork() {

const root = new Error("Database timeout");

const err = new Error("Checkout failed", { cause: root });

err.code = "CHECKOUT_TIMEOUT";

err.meta = { orderId: "ord_7712", retryable: true };

throw err;

}

try {

doWork();

} catch (err) {

console.dir(err, { depth: 6, showHidden: true });

}

When I’m debugging, I want to see the full error shape. In Node.js, internal fields and non-enumerables can matter, so showHidden: true is sometimes the only way to see what’s really attached.

Recipe 2: Understand Buffers and typed arrays without guessing

Buffers are everywhere in Node: crypto, files, network protocols. Printing them poorly is a fast path to confusion.

// save as dir-buffer.js

import crypto from "node:crypto";

const buf = crypto.randomBytes(16);

console.dir({

length: buf.length,

hex: buf.toString("hex"),

bufferPreview: buf,

}, {

depth: 3,

colors: true,

maxArrayLength: 50,

});

I include both a human-friendly representation (hex) and the raw Buffer. That combination answers “what is it?” and “what exactly are the bytes?” without extra steps.

Recipe 3: Spot why an object won’t serialize

If your API is returning an empty object or your cache layer refuses a value, you’re often holding something non-serializable: functions, BigInt, symbols, class instances, circular refs.

console.dir() helps you see those patterns quickly.

// save as dir-serialization.js

const cacheValue = {

key: "profile:cus_20491",

updatedAt: new Date(),

// JSON.stringify will drop this

compute: () => "not serializable",

// JSON.stringify will throw on BigInt

version: 2n,

};

console.dir(cacheValue, { depth: 4, showHidden: true });

If you’re moving data across process boundaries (queues, caches, RPC), you should treat “inspect before serialize” as a normal step.

Recipe 4: Debugging HTTP server objects without dumping the universe

Server request/response objects are huge. You don’t want the whole thing; you want the few fields that shape behavior.

// save as dir-http-targeted.js

import http from "node:http";

const server = http.createServer((req, res) => {

console.dir(

{

method: req.method,

url: req.url,

headers: req.headers,

httpVersion: req.httpVersion,

socket: {

remoteAddress: req.socket.remoteAddress,

remotePort: req.socket.remotePort,

},

},

{ depth: 5, colors: true, maxStringLength: 200 }

);

res.statusCode = 200;

res.end("ok");

});

server.listen(3000, () => {

console.log("listening on http://localhost:3000");

});

I intentionally build a small object and inspect that. You get clarity without noise.

Choosing the right tool: console.dir() vs alternatives (with a modern recommendation)

Here’s how I decide, in practice.

Goal

Traditional approach

Modern approach I recommend (2026) —

— Quick print during debugging

console.log(obj)

console.dir(obj, { depth: 5, colors: true }) for anything nested or unclear See non-enumerables / symbols

Ignore them (and get confused)

console.dir(obj, { showHidden: true }) when you suspect library internals Avoid huge log spam

Print everything and regret it

Clamp with maxArrayLength, maxStringLength, reasonable depth Stable production logs

console.log(JSON.stringify(...))

Structured logger (for example, JSON logs) + explicit redaction Compare object shapes across runs

Read raw output manually

sorted: true + consistent options + snapshot tests when appropriate Inspect an object that custom-prints

Accept the “nice” view

customInspect: false to see raw fields

My blunt guidance:

  • Use console.dir() when your main question is “what’s inside this object?”
  • Use structured logging when your main question is “what happened in production over time?”

If you try to make console.dir() your production logging strategy, you’ll end up with noisy, inconsistent logs. If you try to make structured logging your interactive debugging tool, you’ll end up writing too much ceremony. Pick the tool that matches the job.

A workflow I actually use in 2026: fast local feedback + AI-assisted inspection

My debugging loop today is tighter than it was a few years ago, mostly because I treat inspection as a first-class step.

Here’s a workflow that consistently works:

1) Start with console.dir() at a conservative depth

  • I’ll do depth: 3 or depth: 5 first.
  • I’ll cap arrays/strings.

2) If I suspect hidden fields, I re-run with showHidden: true

  • I only do this when I’m dealing with framework objects, errors, or instances.

3) If the output looks “too neat,” I disable custom inspection

  • customInspect: false is my reality check.

4) I isolate the shape into a small, explicit object

  • Instead of printing req or an ORM model directly, I pick the 8–15 fields that matter.

5) I use AI to turn the printed shape into targeted follow-ups

  • When the object is unfamiliar (third-party SDK, complex internal model), I’ll paste the inspected shape into my assistant and ask:

– “Which fields look like identifiers vs derived values?”

– “What’s the minimal subset to log safely?”

– “Where might a circular reference come from here?”

The key is that console.dir() gives you a faithful map of what you’re holding. Once you can see the map, you can ask better questions, write smaller reproductions, and fix the root cause instead of treating symptoms.

Common mistakes I’d fix in your codebase this week

If I were reviewing a Node.js codebase and I saw these patterns, I’d change them immediately:

  • Printing giant objects directly (console.log(req)), then complaining that logs are unreadable
  • Using %j formatting for anything non-trivial (it trains you to trust a lossy view)
  • Setting depth: null by default everywhere (you’ll eventually hang your terminal on a huge object)
  • Debugging errors with only err.message (you miss cause, codes, and attached metadata)
  • Logging secrets because “it’s just dev” (dev logs get copied, screenshotted, pasted, and shipped)

If you adopt just one habit: build a small “debug view” object and pass it to console.dir() with sane options. That habit scales from tiny scripts to large services.

Key takeaways and what I’d do next

When you’re debugging Node.js, the hardest part is rarely the syntax—it’s the shape of the data at the exact moment something goes wrong. console.dir() is the fastest way I know to stop hand-waving and start seeing.

  • Treat console.dir() as an inspection tool, not a logging strategy. Use it to learn the structure of unfamiliar objects, confirm assumptions, and expose nested state.
  • Use inspection options intentionally. I typically start with depth: 3 to depth: 6, turn on colors in terminals, and clamp output with maxArrayLength and maxStringLength when payloads get big.
  • Reach for showHidden: true when you suspect non-enumerable or symbol properties (common with framework objects and errors). Reach for customInspect: false when the output looks “friendly” but incomplete.
  • Protect yourself from self-inflicted problems: avoid dumping huge graphs, be cautious around getters, and redact secrets before you print anything that might contain tokens or cookies.

If you want a concrete next step, pick one gnarly object in your project—an HTTP request, a database model instance, or a complicated error path—and replace a few console.log() calls with console.dir() using explicit options. You’ll notice two things right away: you’ll read your logs faster, and you’ll trust what you’re seeing. That trust is what turns debugging from guessing into engineering.

Scroll to Top