Why you’re seeing “[object Object]” (and why it matters)
If you’ve built anything in JavaScript—front-end UI, Node.js services, build tooling—chances are you’ve encountered the mysterious output:
[object Object]
It shows up in places you don’t want it: in DOM text, in logs, in error messages, in HTTP responses, and sometimes in production telemetry. It’s rarely the root problem; it’s a symptom that you’re implicitly converting an object into a string without deciding how it should be represented.
For junior developers, this is often confusing (“Why didn’t it print my object?”). For senior engineers, it’s a sign of leaky abstractions: logging that hides structure, UI rendering that doesn’t define formatting rules, or APIs returning inconsistent payloads.
The goal of this guide is to help you:
- Understand exactly where
"[object Object]"comes from. - Debug it quickly in browsers, Node, and frameworks.
- Choose the right representation (JSON, inspect, custom formatting).
- Prevent it through better logging, serialization contracts, and UI design.
1) The technical root cause: object-to-string coercion
In JavaScript, objects are not strings. When an object is used in a context where a string is expected, JavaScript performs type coercion. Unless the object defines a custom string conversion, the default is effectively:
jsObject.prototype.toString.call({}) // "[object Object]"
This comes from Object.prototype.toString, which returns "[object " + internalClass + "]". For plain objects, the “class” is Object.
Common coercion triggers
You’ll get "[object Object]" when you do things like:
jsconst obj = { a: 1 }; "Value: " + obj // "Value: [object Object]" `${obj}` // "[object Object]" String(obj) // "[object Object]" obj.toString() // "[object Object]" (unless overridden) // In the DOM someElement.textContent = obj; // "[object Object]" // In query params or URL building const url = "/search?q=" + obj; // ...q=[object Object]
Note: In many devtools consoles, console.log(obj) displays the object as an interactive structure—but that’s not the same as converting it to a string. The “bad” output tends to appear when concatenation, templating, or string APIs are involved.
2) Diagnose the location: where is the coercion happening?
Before fixing, determine where the object becomes a string.
A. Look for implicit concatenation
Search your code for patterns like:
"" + value"prefix" + value- Template literals:
`${value}` - String APIs:
replace,match,includes,splitwith non-strings - DOM assignment:
textContent,innerText, sometimesinnerHTML
Example bug:
jsfunction renderUser(user) { return `User: ${user}`; // user is { id, name } }
Fix by picking a representation:
jsfunction renderUser(user) { return `User: ${user.name} (#${user.id})`; // or JSON.stringify(user) when appropriate }
B. Add a breakpoint on coercion points
In Chrome/Edge DevTools, you can set breakpoints in your rendering functions or in code that assigns DOM text. If you don’t know where it happens, add a quick assertion:
jsfunction assertNotObjectForText(value) { if (value && typeof value === "object") { debugger; // inspect stack throw new Error("Attempted to render object as text"); } return value; } element.textContent = assertNotObjectForText(maybeObject);
This “fail fast” pattern is extremely effective when [object Object] appears deep in UI layers.
C. Use stack traces from errors and warnings
If the bad string ends up in a request URL or an error message, capture the stack:
jsconst s = String(obj); console.trace("Coerced to string", s);
In Node.js, console.trace and structured logs (JSON) can make these issues much more discoverable.
3) Console output vs real strings: a frequent confusion
A classic trap:
jsconsole.log({ a: 1 });
In many environments this prints:
- Browser: an expandable object tree
- Node:
{ a: 1 }
But this is not string conversion; it’s devtools rendering.
Compare:
jsconsole.log("" + { a: 1 }); // "[object Object]" console.log(String({ a: 1 })); // "[object Object]" console.log(JSON.stringify({ a: 1 })); // "{"a":1}"
If you’re shipping logs to a centralized system, prefer structured logging where the log payload remains an object (not pre-stringified). When forced into string-only sinks, stringify intentionally.
4) Choosing the right representation: JSON, inspect, or custom format
Option 1: JSON.stringify (good for data, APIs, logs)
jsJSON.stringify({ a: 1, b: 2 }) // '{"a":1,"b":2}'
Pretty-printing:
jsJSON.stringify(obj, null, 2)
Pros
- Standard, interoperable
- Great for network payloads and storage
Cons
- Fails on circular references
- Drops
undefined, functions, symbols - Converts
Dateto ISO string (viatoJSON) - BigInt throws unless handled
Circular reference handling (safe stringify):
jsfunction safeStringify(value) { const seen = new WeakSet(); return JSON.stringify(value, (key, val) => { if (typeof val === "object" && val !== null) { if (seen.has(val)) return "[Circular]"; seen.add(val); } if (typeof val === "bigint") return val.toString(); return val; }); }
Option 2: Node.js util.inspect (best for debugging)
In Node:
jsimport util from "node:util"; console.log(util.inspect(obj, { depth: 5, colors: true }));
Pros
- Handles circular references gracefully
- Preserves types and is often more readable than JSON
Cons
- Not a standard data interchange format
- Not ideal for APIs or long-term persistence
Option 3: Define your own toString / toJSON
For domain entities, intentional stringification can prevent [object Object] and improve debugging.
jsclass User { constructor(id, name) { this.id = id; this.name = name; } toString() { return `User(${this.id}, ${this.name})`; } toJSON() { return { id: this.id, name: this.name }; } } const u = new User(1, "Ada"); String(u); // "User(1, Ada)" JSON.stringify(u); // '{"id":1,"name":"Ada"}'
This is especially useful when objects might appear in template literals or error messages.
Option 4: In browsers, console.dir, console.table
console.dir(obj)displays properties more explicitly.console.table(arrayOfObjects)is great for lists.
These don’t solve coercion, but they reduce the need to stringify in the first place.
5) Front-end scenarios: DOM, React, Vue, Angular
A. Vanilla DOM rendering
This is the most direct route to [object Object]:
jsdocument.querySelector("#output").textContent = { a: 1 }; // renders: [object Object]
Fix by rendering a field or formatting:
jsoutput.textContent = JSON.stringify({ a: 1 }, null, 2); output.style.whiteSpace = "pre";
B. React: “Objects are not valid as a React child” vs [object Object]
React often throws if you try to render an object as a child:
jsxexport function Profile({ user }) { return <div>{user}</div>; // throws in many cases }
But you can still end up with [object Object] when you coerce:
jsxreturn <div>{String(user)}</div>; // renders [object Object]
Better:
jsxreturn <div>{user.name}</div>; // or return <pre>{JSON.stringify(user, null, 2)}</pre>;
C. Vue templates
Vue will stringify values in interpolations:
html<div>{{ user }}</div>
If user is a plain object, you may see [object Object]. Fix with a computed property:
jscomputed: { userText() { return `${this.user.name} (#${this.user.id})`; } }
Or a safe JSON render for debugging UI:
html<pre>{{ JSON.stringify(user, null, 2) }}</pre>
D. Angular interpolation
Angular tends to render objects as [object Object] in templates:
html<div>{{ user }}</div>
Use the built-in json pipe for debug:
html<pre>{{ user | json }}</pre>
But for production UI, format explicitly.
6) Back-end / API scenarios: Express, Fastify, and querystrings
A. Express response mistakes
If you do:
jsres.send({ ok: true });
Express will typically serialize JSON correctly. But if you do:
jsres.send("" + { ok: true });
You’ll send [object Object].
Also watch out for logging middleware that concatenates objects:
jsconsole.log("request body: " + req.body); // [object Object]
Use:
jsconsole.log("request body:", req.body); // or console.log("request body:", JSON.stringify(req.body));
B. Querystring bugs
This is a very common production footgun:
jsconst params = { filter: { status: "open" } }; fetch("/api/items?filter=" + params.filter); // ...?filter=[object Object]
Solution: use URLSearchParams and explicit serialization rules.
Simple approach:
jsconst qs = new URLSearchParams({ filter: JSON.stringify({ status: "open" }) }); fetch(`/api/items?${qs.toString()}`);
More robust approach: adopt a querystring library with nested support (e.g., qs) and make it consistent across client/server.
7) Tooling & library comparisons for safe serialization
JSON.stringify
- Best default for APIs and structured logs.
- Breaks on circular references and BigInt.
util.inspect (Node)
- Best for developer-facing diagnostics.
- Not for interoperability.
“Safe stringify” libraries
Common options include:
fast-safe-stringifysafe-stable-stringify
What to look for:
- Circular reference handling
- Stable key ordering (useful for tests/diffing)
- BigInt support
- Performance on large objects
If you operate at scale (large payloads, high QPS), benchmark stringify performance. A surprising amount of CPU can go into serialization.
8) Debugging techniques that scale beyond console.log
A. Structured logging (recommended)
Instead of building strings:
jslogger.info("User created: " + user);
Prefer:
jslogger.info({ userId: user.id, user }, "User created");
Most modern loggers (pino, bunyan, winston with JSON transports) will keep object structure, enabling filtering and searching.
B. Log sampling and redaction
A big reason developers stringify objects is convenience. The better approach is to log objects safely:
- Redact secrets (tokens, passwords, cookies).
- Limit depth and size.
- Avoid dumping full request objects.
Example (pino-style conceptually):
jsconst logger = pino({ redact: { paths: ["req.headers.authorization", "password", "token"], censor: "[REDACTED]" } });
C. Use assertions to catch wrong types early
In TypeScript, many [object Object] issues are type-level “any leaks.” Add constraints.
Example:
tsfunction setLabelText(el: HTMLElement, text: string) { el.textContent = text; } // If you accidentally pass an object, TS will catch it.
In runtime JS, validate:
jsfunction ensureString(value) { if (typeof value !== "string") { throw new TypeError(`Expected string, got ${typeof value}`); } return value; }
D. Repro with minimal cases
When you see [object Object] in a large app, isolate:
- Find the final string (DOM node text, log line, URL).
- Search for it in code or inspect network request.
- Put breakpoints on setters (
textContentassignments, request builders). - Walk stack to identify the conversion.
This works better than grepping for “toString” because coercion is often implicit.
9) Preventing [object Object] with better design
A. Define “presentation” as a separate concern
Don’t rely on default conversion for domain data.
Bad:
jsrender(`Order: ${order}`)
Good:
jsrender(`Order: ${formatOrder(order)}`) function formatOrder(order) { return `#${order.id} (${order.status})`; }
B. Prefer explicit field selection in UIs
UI components should not accept “any object” when they only need a string.
React example:
jsxfunction UserBadge({ name }) { return <span className="badge">{name}</span>; } // caller passes user.name, not user <UserBadge name={user.name} />
This prevents accidental object rendering.
C. Adopt serialization contracts for APIs
If you pass objects through:
- query params
- postMessage
- localStorage/sessionStorage
- Redis caches
…define a contract:
- JSON only
- stable schema
- versioned if needed
For localStorage:
jslocalStorage.setItem("user", JSON.stringify(user)); const stored = JSON.parse(localStorage.getItem("user") || "null");
Never do:
jslocalStorage.setItem("user", user); // will store "[object Object]"
D. Enforce linting rules
ESLint can prevent common coercion patterns.
Consider rules / practices:
- Disallow
"" + valueunless value is known string. - Encourage template literals only with primitives.
- Use TypeScript +
noImplicitAny.
Even without a dedicated rule, code review checklists can target:
- logging concatenation
- URL building
- DOM text assignments
10) Edge cases: Dates, Errors, Maps/Sets, and custom objects
A. Date
jsString(new Date()) // nice human-readable date string JSON.stringify({ when: new Date() }) // {"when":"2026-05-25T...Z"}
That might be what you want—or not. Decide explicitly.
B. Error
Errors stringify poorly:
jsString(new Error("boom")) // "Error: boom" (ok-ish) JSON.stringify(new Error("boom")) // "{}" in many cases
For error logging, extract fields:
jsfunction serializeError(err) { return { name: err.name, message: err.message, stack: err.stack, cause: err.cause }; }
C. Map and Set
jsJSON.stringify(new Map([['a', 1]])) // "{}" (not useful)
Convert explicitly:
jsconst map = new Map([['a', 1]]); JSON.stringify(Object.fromEntries(map)); const set = new Set([1,2,3]); JSON.stringify(Array.from(set));
D. Customizing coercion with Symbol.toPrimitive
If you truly want custom behavior for string contexts:
jsconst user = { id: 1, name: "Ada", [Symbol.toPrimitive](hint) { if (hint === "string") return `User#${this.id}:${this.name}`; return this.id; } }; `${user}` // "User#1:Ada" user + 1 // 2
This is powerful but can be surprising; use sparingly.
11) Practical checklist: fix [object Object] fast
- Find the exact output location (DOM node, URL, log line, toast message).
- Search for string concatenation near that path.
- Add a runtime guard to throw on object-to-text rendering.
- Decide the correct representation:
- UI: pick fields, format domain-specific strings.
- Debug:
console.dir,console.table,util.inspect. - Storage/API:
JSON.stringifywith a schema.
- Handle edge cases (circular refs, BigInt, Error, Map/Set).
- Prevent regression:
- add TypeScript types / strict mode
- add lint rules or tests
- adopt structured logging
12) Example: end-to-end fix in a realistic scenario
Problem
A front-end app shows a toast:
Saved: [object Object]
Code:
jsasync function saveSettings(settings) { const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings) }); const saved = await res.json(); showToast(`Saved: ${saved}`); }
saved is an object like { id, updatedAt }.
Fix
Define what “saved” means:
jsfunction formatSavedResult(saved) { // Explicit UI contract return `#${saved.id} at ${new Date(saved.updatedAt).toLocaleString()}`; } async function saveSettings(settings) { const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings) }); const saved = await res.json(); showToast(`Saved: ${formatSavedResult(saved)}`); }
Add a guard to prevent recurrence
jsfunction showToast(message) { if (typeof message !== "string") { throw new TypeError("Toast message must be a string"); } // render toast... }
This turns a vague UI symptom into a clear, actionable error during development.
13) Best practices summary
[object Object]almost always means implicit string coercion.- Prefer explicit formatting in UI code: select fields, use formatter functions.
- Prefer structured logging and avoid concatenating objects into strings.
- Use
JSON.stringifyfor storage and APIs—but handle circular refs and BigInt when needed. - Use
util.inspect(Node) or devtools helpers (console.dir,console.table) for debugging. - Add runtime guards and TypeScript typings to stop accidental object rendering.
With a few disciplined conventions—explicit formatting, structured logs, and minimal coercion—you can turn [object Object] from an annoying mystery into a rare and quickly fixable issue.
![Turning “[object Object]” Into Actionable Insight: Debugging, Preventing, and Designing Better Object Output in JavaScript](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_614921a108.png)