Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

JavaScript Echo to Console: Beyond console.log

Sean

Platform Writer

Aug 30, 2026
8 min read

console.log(value) prints to the console. The trap is that logging an object stores a live reference, so what you see in devtools may be the object as it is now, not as it was when you logged it.

JavaScript Echo to Console: Beyond console.log

Printing to the console is the first thing anyone learns and one of the last things anyone gets good at. console.log covers most of it, and the rest of the console API — table, group, time, dir — replaces a surprising amount of manual formatting.

The part actually worth your attention is the object-reference behaviour, because it makes the console lie to you at exactly the moment you are relying on it.

Table of contents

Multiple arguments beat string concatenation

// Fine
console.log("user:", user, "count:", count);

// Worse -- objects become "[object Object]"
console.log("user: " + user + " count: " + count);

Separate arguments are logged as values, so objects stay inspectable and numbers stay numbers. Concatenation stringifies everything, and [object Object] tells you nothing.

There are also format specifiers, which are useful for consistent output:

console.log("%s deployed in %dms", service, elapsed);
console.log("%c Deployed ", "background: #16a34a; color: white; padding: 2px 6px;");
console.log("%o", element);  // %o object, %j JSON in Node, %i integer, %f float

%c applies CSS to the rest of the line in browser devtools, which is how libraries produce those coloured banner messages. Mildly indulgent, genuinely useful for making one class of log stand out.

The lazy-evaluation trap

This is the one that costs people real debugging time:

const state = { status: "pending" };
console.log(state);        // devtools shows { status: "succeeded" } ?!
state.status = "succeeded";

Browser devtools store a reference to the object and render its properties when you expand the entry in the panel — which happens after your code has moved on and mutated it. The log line is correct; the expanded view shows the present.

Three ways out, in order of preference:

console.log(structuredClone(state));      // a real snapshot
console.log(JSON.stringify(state));      // string, loses functions and undefined
console.table([{ ...state }]);           // shallow copy, formatted

structuredClone is built into modern browsers and Node, handles nested objects, Maps, Sets and Dates, and gives you a genuine snapshot. It is the right default when logging mutable state.

Note that Node’s console does not have this problem — it serialises at call time. This is browser devtools behaviour specifically, which is why the bug is confusing when it only reproduces in one place.

The methods that replace manual formatting

// Arrays of objects as a real table, with sortable columns
console.table(users);
console.table(users, ["id", "email"]);  // only these columns

// Collapsible groups -- excellent for nested or repeated work
console.group("Deploy: api-service");
console.log("build ok");
console.groupEnd();

// Timing without Date.now() arithmetic
console.time("query");
await db.query(sql);
console.timeEnd("query");   // query: 42.31ms

// Log only when something is wrong
console.assert(res.ok, "request failed", res.status);

// How many times did this run?
console.count("render");

// The object's properties, not its DOM representation
console.dir(element);

// Where am I being called from?
console.trace("unexpected call path");

console.table is the standout. Any array of objects becomes a sortable table, and it turns “scroll through 40 expanded objects looking for the odd one” into a glance.

console.trace is the one to remember when a function is being called and you cannot work out from where. It prints the full stack without throwing anything.

console.error and console.warn are not just colours — they capture a stack trace, and devtools can filter by level. Use the right one, and error tracking tools will pick them up.

Node is a different environment

In Node, console.log writes to stdout and console.error to stderr. That distinction is operationally important, because process managers, containers and log collectors treat the two streams differently — and it means you can redirect diagnostics away from data output:

node script.js > output.json 2> errors.log

Node also truncates nested objects at depth 2 by default, which is why deeply nested structures print as [Object]:

console.dir(obj, { depth: null, colors: true });
// or
console.log(util.inspect(obj, { depth: null, maxArrayLength: null }));

And console.log in Node is synchronous when writing to a file or pipe. In a hot request path, logging blocks the event loop — one of those things that is invisible at low traffic and measurable at high traffic.

What to do in production

Console calls left in shipped code are a mild liability: they leak internal state to anyone with devtools open, they cost performance in hot paths, and they hold references that prevent garbage collection.

Options, roughly in order of seriousness:

  • Strip them at build time. Most bundlers can drop console.* calls in production builds. Simple, and it makes the console-in-source question moot.
  • Lint against them. no-console in ESLint, with an allowance for console.error and console.warn.
  • Use a real logger. In Node, a structured logger emitting JSON with levels, timestamps and request IDs is what makes logs searchable rather than scrollable.

The third is the real answer for a service. console.log("user created") is unsearchable; log.info({ userId, requestId }, "user created") can be filtered by user across every instance. The moment you have more than one instance, that difference decides whether logs are useful.

Logs you can actually find

Structured logging only pays off if the logs are somewhere you can read them. On a laptop, console.log and a terminal are the whole system. In production, output goes to stdout and something has to collect it, or it is gone with the container.

The practical requirement is that the deploy that shipped a change and the runtime logs from that deploy are in the same place, so a log line can be tied to a build. That is what turns “something started erroring on Tuesday” into “this deploy started erroring”.

On RunxBuild, services carry build logs and runtime logs per deploy, with rollback to a previous deploy, so a bad change is identified and reverted from the same screen where its output is.

How this fits the rest of the stack

console.log with multiple arguments covers most cases; structuredClone fixes the object-reference trap; and table, group, time and trace replace a good deal of hand formatting. In production, strip console calls or move to a structured logger — unsearchable logs are barely logs. The RunxBuild hosting calculator shows what the service producing them costs to run alongside its database.

Useful related references:

FAQ

Why does console.log show a different object than expected?

Browser devtools store a reference and render properties when you expand the entry, which is after your code has mutated the object. Log structuredClone(obj) for a real snapshot. Node does not have this problem because it serialises at call time.

What is the difference between console.log and console.error?

In the browser, console.error captures a stack trace and is filterable by level in devtools. In Node, console.log writes to stdout and console.error to stderr, which log collectors and shell redirection treat separately.

Why does Node print [Object] instead of nested properties?

Node truncates object inspection at depth 2 by default. Use console.dir(obj, { depth: null }) or util.inspect(obj, { depth: null }) to see the whole structure.

Should I remove console.log before deploying?

Yes, or strip it at build time. Console calls leak internal state to anyone with devtools open, cost performance in hot paths, and hold references that prevent garbage collection. For services, replace them with a structured logger.

What is console.table useful for?

Rendering an array of objects as a sortable table with one column per property, optionally limited to named columns. It replaces scrolling through dozens of expanded objects when you are looking for the one that differs.

#console.log#JavaScript debugging#browser devtools#Node logging#JavaScript