Why you’re seeing “[object Object]”
If you’ve built anything in JavaScript—frontend UI, Node.js services, CLI tools—you’ve probably encountered an output like:
[object Object]
It’s one of the most common “what just happened?” moments, especially for juniors. For seniors, it’s often a sign of a subtle bug: incorrect string interpolation, accidental implicit coercion, or logging that hides useful context.
At a high level, [object Object] is the default string representation of a plain JavaScript object when it’s coerced into a string.
The core rule: implicit string conversion
In JavaScript, many operations cause values to be converted to strings automatically:
- String concatenation:
"value: " + something - Template literals:
`value: ${something}`(this callstoString()under the hood) - DOM assignment:
element.textContent = someObject as any(object becomes a string) - Throwing errors with objects in message strings
- Using objects as keys in some contexts
For a plain object, the default Object.prototype.toString() returns:
js({}).toString(); // "[object Object]"
That’s exactly where the text comes from.
What it actually means (and what it doesn’t)
It means:
- You passed an object to something expecting a string, or
- You combined an object with a string, or
- You rendered/logged in a way that forces
toString(), losing structure.
It doesn’t mean:
- The object is invalid
- The object is empty
- The runtime “broke”
It’s just not being displayed in a useful way.
Common places it appears
1) String concatenation and template strings
jsconst user = { id: 1, name: "Ada" }; console.log("User: " + user); // User: [object Object] console.log(`User: ${user}`); // User: [object Object]
Fix: log the object directly, or serialize it:
jsconsole.log("User:", user); console.log("User:", JSON.stringify(user)); console.log("User:\n", JSON.stringify(user, null, 2));
2) React rendering errors
React famously complains when you try to render an object:
jsxfunction Profile({ user }) { return <div>{user}</div>; // error: Objects are not valid as a React child }
Sometimes developers “fix” it accidentally by stringifying and end up with "[object Object]" (or a React error). The correct fix is to render the right fields:
jsxfunction Profile({ user }) { return <div>{user.name}</div>; }
Or for debugging:
jsx<pre>{JSON.stringify(user, null, 2)}</pre>
3) DOM APIs expecting strings
jsconst el = document.querySelector("#output"); el.textContent = { status: "ok" }; // becomes "[object Object]"
Fix:
jsel.textContent = JSON.stringify({ status: "ok" }, null, 2);
4) URL/querystring construction
jsconst params = { q: "test", page: 2 }; const url = "/search?" + params; // /search?[object Object]
Fix: use URLSearchParams:
jsconst params = new URLSearchParams({ q: "test", page: "2" }); const url = `/search?${params.toString()}`;
For nested objects/arrays, you’ll need a library like qs.
5) Node.js logging differences (console vs concatenation)
In Node.js, console.log(obj) prints an inspect view, but concatenation forces coercion:
jsconst obj = { a: 1 }; console.log(obj); // { a: 1 } console.log("" + obj); // [object Object]
Fix: avoid concatenation when logging.
Under the hood: toString(), valueOf(), and Symbol.toPrimitive
When JavaScript needs to convert an object to a primitive (string/number), it follows a protocol:
- If the object has a
Symbol.toPrimitivemethod, it uses that. - Otherwise it tries
valueOf()andtoString()depending on the hint. - For most objects with default implementations,
toString()yields"[object Object]".
Demonstration:
jsconst obj = {}; obj.valueOf(); // {} obj.toString(); // "[object Object]"
You can customize this behavior:
jsconst user = { id: 1, name: "Ada", toString() { return `User(${this.id}, ${this.name})`; } }; String(user); // "User(1, Ada)"
Or using Symbol.toPrimitive for more control:
jsconst money = { amount: 12.34, currency: "USD", [Symbol.toPrimitive](hint) { if (hint === "number") return this.amount; return `${this.amount.toFixed(2)} ${this.currency}`; } }; `${money}`; // "12.34 USD" +money; // 12.34
Best practice: customizing toString() can be useful for domain objects, but for debugging/logging you usually want structured output (JSON/inspect), not a custom string.
Debugging: finding where the coercion happens
The hard part is often not “how to print an object,” but where your object got coerced into a string.
1) Search for suspicious patterns
Look for:
"..." + something- Template literals:
`${something}` String(something).textContent = ...,.innerHTML = ...- Query building:
"?" + params - Logging wrappers that do
message + data
2) Break on exceptions (frontend)
In Chrome DevTools:
- Enable Pause on exceptions
- If React/Vue throws due to object rendering, you’ll land at the point of the render
Even if you don’t get an exception, you can set breakpoints at the call sites.
3) Add “type + preview” logs
Instead of console.log("value=" + value), log structure:
jsconsole.log({ type: typeof value, isArray: Array.isArray(value), value, });
If value is null, note:
jstypeof null === "object" // yes, historical quirk
4) Use console.dir and console.table
console.dir(obj)emphasizes propertiesconsole.table(arrayOfObjects)is great for lists
jsconsole.dir(user, { depth: null }); console.table(users);
In Node.js, prefer util.inspect for precise control:
jsimport util from "node:util"; console.log(util.inspect(obj, { depth: null, colors: true }));
5) Track unexpected stringification with stack traces
If you suspect a function is being stringified somewhere, wrap it:
jsfunction debugStringify(label, value) { console.trace(`Stringify called for ${label}`); return JSON.stringify(value); }
Or, if you own the object, temporarily add:
jsconst obj = { a: 1, toString() { console.trace("toString called"); return "[custom toString]"; } };
This is a powerful trick: you can reveal where coercion is happening.
Fix patterns: choosing the right representation
Not all fixes are JSON.stringify. The right approach depends on context.
Pattern A: Log objects as separate arguments
Most consoles (browser/Node) handle structured logging well:
jslogger.info("user loaded", { userId: user.id, user });
Avoid:
jslogger.info("user loaded: " + user); // [object Object]
Pattern B: JSON serialization (with caveats)
For readable output:
jsJSON.stringify(user, null, 2)
Caveats:
- Drops functions and symbols
- Converts
Dateto ISO string viatoJSON - Fails on circular references
- Loses
Map,Set,BigInt(BigInt throws), and custom classes
Handling circular references
Circular references are common with complex state trees and DOM nodes.
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
Options:
- Use a “safe stringify” implementation:
jsfunction safeStringify(value, space = 2) { 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; }, space); }
- Use a library:
- fast-safe-stringify (good performance)
- flatted (supports circular data but uses a custom format)
For logging, returning "[Circular]" markers is usually enough.
Pattern C: Render specific fields (UI best practice)
If user is an object, decide which fields belong in the UI.
jsx<div> <h2>{user.name}</h2> <p>ID: {user.id}</p> </div>
Use <pre> with JSON only as a debug view, not a final UI.
Pattern D: Use URLSearchParams or qs for query strings
URLSearchParamsworks great for flat key/value pairs.- For nested objects, arrays, and Rails-style syntax, use
qs.
Example with qs:
jsimport qs from "qs"; const params = { filter: { status: ["open", "closed"] }, page: 2 }; const query = qs.stringify(params, { arrayFormat: "brackets" }); // filter[status][]=open&filter[status][]=closed&page=2
Pattern E: Provide domain-level toString() carefully
For errors and domain objects, a meaningful string can help:
tsclass UserId { constructor(readonly value: string) {} toString() { return this.value; } } const id = new UserId("u_123"); console.log(`loading user ${id}`); // loading user u_123
But don’t rely on toString() for structured logging; keep both options.
Framework-specific notes
React
- React will not render objects as children.
- Arrays of primitives are fine; arrays of objects require mapping.
Bad:
jsxreturn <div>{users}</div>; // users: [{...}, {...}] => error
Good:
jsxreturn ( <ul> {users.map(u => <li key={u.id}>{u.name}</li>)} </ul> );
Debugging tip: when you see an error mentioning “Objects are not valid as a React child”, identify which prop is an object and render the right property.
Vue
Vue templates will stringify objects in some contexts (often producing "[object Object]"):
html<div>{{ user }}</div>
Fix:
html<div>{{ user.name }}</div> <pre>{{ JSON.stringify(user, null, 2) }}</pre>
Angular
Angular interpolation also stringifies:
html<div>{{ user }}</div>
Use the built-in json pipe for debugging:
html<pre>{{ user | json }}</pre>
Logging tool comparisons (practical guidance)
console.log vs structured loggers
console.logis fine for local debugging.- For services, use structured logging so logs are machine-queryable.
Popular Node.js loggers:
- pino: very fast, JSON logs by default, great for production
- winston: flexible transports, more configuration-heavy
- bunyan: older, still used in some stacks
Example with pino:
jsimport pino from "pino"; const logger = pino(); logger.info({ user }, "loaded user");
This avoids "[object Object]" entirely because the object is preserved as JSON.
Browser logging
In browsers, prefer:
jsconsole.log("state", state); console.log({ state });
Note: some browser consoles display objects lazily (the “live object” issue). If you expand later, you may see updated values rather than what existed at log time.
To capture a snapshot:
jsconsole.log("snapshot", structuredClone(state));
Or serialize:
jsconsole.log("snapshot", JSON.parse(JSON.stringify(state)));
(Serialization loses non-JSON types; structuredClone preserves more, but not functions.)
Best practices to prevent “[object Object]” bugs
1) Avoid implicit coercion in production code
Prefer explicit formatting:
js// good logger.info({ userId: user.id }, "loaded user"); // risky logger.info("loaded user " + user); // forces string conversion
2) Use TypeScript to make string/objects mismatches obvious
In TS, a lot of these issues are revealed when you type things correctly.
tsfunction setLabel(text: string) { document.querySelector("#label")!.textContent = text; } const user = { name: "Ada" }; setLabel(user); // Type error: not assignable to string
If you see this compile, it often means any leaked in. Track any sources and tighten types.
Helpful TS settings:
json{ "compilerOptions": { "strict": true, "noImplicitAny": true, "noUncheckedIndexedAccess": true } }
3) Create small formatting utilities
Instead of sprinkling JSON.stringify everywhere, centralize:
tsexport function formatForLog(value: unknown): string { if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); if (value instanceof Error) return value.stack || value.message; try { return safeStringify(value, 2); } catch { return String(value); } }
Then use:
tslogger.error(`request failed: ${formatForLog(err)}`);
4) Be careful with Error construction
This is a frequent source of "[object Object]":
jsthrow new Error("Request failed: " + response);
Instead:
jsthrow new Error(`Request failed: ${response.status} ${response.statusText}`);
And attach structured context:
jsconst err = new Error("Request failed"); err.cause = { status: response.status, body: data }; // Node 16+ supports cause patterns throw err;
Or with modern Error options:
jsthrow new Error("Request failed", { cause: { status: response.status, data } });
5) Understand how your templating system stringifies
- React: objects as children = error
- Vue/Angular: objects =
"[object Object]" - Server templates (Handlebars/EJS): often call
toString()
When in doubt, render a specific property or use a JSON debug view.
Real-world scenarios and fixes
Scenario 1: Fetch response logged as “[object Object]”
jsconst res = await fetch("/api/user"); console.log("res=" + res); // [object Object]
Fix:
jsconsole.log("status", res.status); console.log("headers", Object.fromEntries(res.headers.entries())); console.log("url", res.url);
If you want the body:
jsconst data = await res.json(); console.log("data", data);
Scenario 2: Express sending an object accidentally as string
jsapp.get("/", (req, res) => { res.send("result: " + { ok: true }); });
Fix:
jsapp.get("/", (req, res) => { res.json({ ok: true }); });
Or:
jsres.type("text/plain").send(`result: ${JSON.stringify({ ok: true })}`);
Scenario 3: FormData debugging
FormData stringifies poorly:
jsconst fd = new FormData(); fd.append("name", "Ada"); console.log("fd=" + fd); // [object FormData] or similar
Instead inspect entries:
jsfor (const [k, v] of fd.entries()) { console.log(k, v); }
Scenario 4: Accidentally using an object as a map key
jsconst map = {}; const key = { id: 1 }; map[key] = "value"; console.log(Object.keys(map)); // ["[object Object]"]
Because object keys in plain objects are strings. Use Map:
jsconst map = new Map(); map.set(key, "value");
Performance and security considerations
Logging large objects
JSON.stringify on huge objects can:
- Block the main thread (frontend)
- Increase CPU usage (backend)
- Flood logs and increase costs
Best practices:
- Log only necessary fields
- Use sampling/rate limiting
- Cap depth/size
Example “pick fields” approach:
jslogger.info({ userId: user.id, role: user.role, requestId, }, "authorized");
Avoid leaking secrets
Dumping full objects often includes:
- tokens
- passwords
- cookies
- PII
Implement redaction in your logger:
pinohas redaction options- For custom code, replace sensitive keys (
password,authorization) with[REDACTED]
Checklist: quick fixes when you see it
-
Is this a string concatenation or template literal?
- Change
"x=" + objtoconsole.log("x=", obj).
- Change
-
Is this UI rendering?
- Render
obj.nameor map over arrays. - Use
<pre>{JSON.stringify(obj, null, 2)}</pre>only for debugging.
- Render
-
Is this a URL/query string?
- Use
URLSearchParamsorqs.
- Use
-
Is this JSON serialization failing due to circular references?
- Use a safe stringify or
util.inspect.
- Use a safe stringify or
-
Is TypeScript letting an object flow into a string?
- Find
anyand tighten types.
- Find
Closing perspective
“[object Object]” is less an error and more a signal: some object got coerced into a string and you lost the structure that mattered. The fix is usually simple—log objects as objects, render specific fields, serialize intentionally—but the engineering skill comes from consistently avoiding implicit coercion, building better debugging habits, and adopting structured logging patterns.
When you treat logs and UI output as designed interfaces (not accidental string concatenations), "[object Object]" goes from an annoying mystery to an early warning that your data formatting layer needs attention.
![Diagnosing and Fixing “[object Object]” in JavaScript: Root Causes, Debugging Techniques, and Best Practices](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_0dc8c62aa1.png)