Your console.log Is Lying to You
Your console.log Is Lying to You
Sun Jun 28 2026
tutorials
javascript
debugging
devtools
... is lying to you
Open your browser DevTools and run this:
const user = { name: "Bob" }<br>console.log(user)<br>user.name = "Alice"
You would expect the log to show { name: "Bob" }, the value at the time of the console.log call. The collapsed line is what you expect:
▶ Object { name: "Bob" }
But expand it, and you will see:
name: "Alice"
Oops. So what's going on? console.log() is the most-used debugging tool in JavaScript, but it can be subtly unreliable. Not because it is broken, but because it optimizes for speed and interactivity rather than for accuracy . It was built for fast exploration in a live, interactive environment, and those priorities come with tradeoffs that can genuinely mislead you during debugging. Over the next sections, we'll look at a few ways the console can mislead you - and, more importantly, why each one exists.
Objects Aren't Snapshots
When you pass an object to console.log() in browser DevTools, the browser does not immediately serialize it into a string. Instead, it stores a live reference to that object and defers the actual rendering until you expand the entry. This is called lazy evaluation, and it is what caused the surprise.
The collapsed ▶ Object you see is essentially a placeholder: the properties shown inside it are evaluated at the moment you click the arrow, not at the moment you called console.log(). By then, your code has already continued running. That means what you're seeing is not a frozen record of the object at the time of logging, but a live view into whatever the object happens to look like when DevTools renders it. In the example:
You log { name: "Bob" }
DevTools stores a reference to the user object
The code continues executing
user.name is mutated to "Alice"
You expand the logged object later and see the current state
This behavior can feel unintuitive at first, because most developers mentally model console.log() as "print this value right now", but in browser DevTools, it is closer to "show me this object as it exists when I look at it".
This design is intentional: if DevTools were to eagerly serialize every object at log time, it would have to deeply traverse and copy potentially large object graphs on every log call. In complex applications, especially ones with frequent logging inside loops or render cycles, that overhead would be expensive in both memory and performance. By deferring evaluation, DevTools stays fast and interactive, even when working with large or constantly changing state.
But that optimization comes with a tradeoff: what you see in the console is not always what existed at the moment you logged it . Once you internalize that distinction (log time vs view time), a whole class of "weird bugs" starts making sense.
The same idea applies to the DOM, as a DOM node is still a JavaScript object, and DevTools may show you its current state when you expand it, not necessarily the state it had when you logged it. If a framework re-rendered the component, changed a class, removed an attribute, or replaced child nodes, an old console.log(element) can be misleading.
When the DOM state matters, snapshot the specific facts:
console.log({<br>path: location.pathname,<br>html: element.outerHTML,<br>text: element.textContent,<br>classes: [...element.classList],<br>disabled: element.disabled,<br>})
The point is the same: preserve the evidence, not just a handle to something that may keep changing.
Promises and Async Timing
Promises change state over time, so a promise that was pending when you logged it may appear fulfilled when you inspect it later. This is related to a deeper async distinction: promises represent future results, not ownership of the work producing those results, which is why cancellation in JavaScript is harder than it looks.
const promise = fetch('https://jsonplaceholder.typicode.com/posts/1')<br>console.log(promise)
You see:
▶ Promise { state>: "pending" }
Expand it after the network resolves:
state>: "fulfilled"<br>value>: Response { type: "cors", url: "https://jsonplaceholder.typicode.com/posts/1", redirected: false, … }<br>prototype>: Promise.prototype { … }
The promise resolved after you logged it, but the console shows both the pending and resolved states depending on when you inspect it. The mechanism differs from the mutable object earlier: the promise genuinely settled from pending to fulfilled once, rather than being mutated repeatedly. The symptom, though, is the same: what you read depends on when you look, not on when you logged.
Unlike objects, DevTools isn't showing a live mutable object here: the promise really did transition from pending to fulfilled exactly once. But the experience is similar: what you learn from the console depends on when you inspect it, not merely when you logged it .
The Heisenbug Effect: Logging Changes Reality
At some point, every JavaScript developer hits this:...