Why you’re seeing [object Object]
[object Object] is JavaScript’s default string representation for a plain object when it’s coerced to a string. It isn’t an “error message” by itself; it’s a symptom that an object was used where a string (or printable value) was expected.
You’ll typically encounter it in places like:
- DOM rendering (e.g.,
innerHTML, template strings, React JSX output) - Logging (console output, server logs, monitoring tools)
- Network payloads (query strings, form submissions)
- Error messages (throwing or concatenating objects)
Understanding how and why coercion happens—and how different environments display values—turns this from an annoying mystery into a predictable, debuggable behavior.
The core mechanism: String coercion and Object.prototype.toString
When JavaScript needs a string—because you used + with a string, a template literal, or assigned to a string-only API—it applies coercion rules:
jsconst obj = { a: 1 }; String(obj); // "[object Object]" obj + ""; // "[object Object]" `Value: ${obj}`; // "Value: [object Object]"
The default toString() on objects is effectively:
jsObject.prototype.toString.call({}); // "[object Object]"
For plain objects, the “tag” is Object, so you get [object Object]. For other types, you may see:
jsObject.prototype.toString.call([]); // "[object Array]" Object.prototype.toString.call(new Date()); // "[object Date]" Object.prototype.toString.call(/re/); // "[object RegExp]"
Why some logs show objects “nicely” but UI shows [object Object]
Browser devtools often render objects interactively when you call console.log(obj). That’s not because the object is a string—devtools has special display logic.
But when you do this:
jsconsole.log("obj=" + obj);
you explicitly force string coercion; now the console can only display the resulting string: obj=[object Object].
Common scenarios and how to fix each
1) UI rendering: DOM APIs and templating
Problem
jsconst user = { name: "Asha", role: "admin" }; document.querySelector("#out").textContent = user; // Displays: [object Object]
textContent expects a string. The object is coerced.
Fixes
- Render a specific field:
jsdocument.querySelector("#out").textContent = user.name;
- Render a formatted representation (for debugging):
jsdocument.querySelector("#out").textContent = JSON.stringify(user, null, 2);
- Render structured UI rather than stringifying:
jsconst out = document.querySelector("#out"); out.innerHTML = ` <div> <strong>${user.name}</strong> <em>${user.role}</em> </div> `;
Note: if you inject strings into
innerHTML, ensure you’re not exposing XSS. PrefertextContentand DOM node creation for untrusted data.
2) React / Vue / Angular rendering
React
jsxfunction Profile({ user }) { return <div>{user}</div>; // [object Object] }
React will attempt to render values; objects aren’t valid children. In some cases you’ll get an error; in other cases you’ll see string coercion depending on surrounding concatenation.
Fix:
jsxfunction Profile({ user }) { return <div>{user.name}</div>; }
For debug display:
jsx<pre>{JSON.stringify(user, null, 2)}</pre>
Vue
html<div>{{ user }}</div>
Vue will stringify to "[object Object]" by default.
Fix:
html<div>{{ user.name }}</div> <pre>{{ JSON.stringify(user, null, 2) }}</pre>
3) Logging: console, server logs, and monitoring
Prefer structured logging (Node.js)
Bad:
jslogger.info("request=" + req); // request=[object Object]
Good:
jslogger.info({ reqId: req.id, url: req.url, headers: req.headers }, "request");
If you need a quick deep print:
jsconst util = require("node:util"); console.log(util.inspect(req, { depth: 5, colors: true }));
Beware: console.log(obj) vs console.log(${obj})
console.log(obj)shows the object in a rich viewer.- Template strings force coercion:
jsconsole.log(`${obj}`); // [object Object]
Avoid logging circular objects with JSON.stringify
Many runtime objects have cycles:
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
Use one of:
util.inspect(Node)flattedpackage- safe replacer
Example safe replacer:
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); } return val; }, space ); } console.log(safeStringify(a));
4) Query strings, forms, and application/x-www-form-urlencoded
A classic place [object Object] appears is when you accidentally send an object in a query string or form field.
Example
jsconst params = new URLSearchParams({ filter: { status: "open" } }); params.toString(); // "filter=%5Bobject+Object%5D"
URLSearchParams expects string values.
Fix: encode as JSON
jsconst params = new URLSearchParams({ filter: JSON.stringify({ status: "open" }) });
On the server, parse it:
jsconst filter = JSON.parse(req.query.filter);
Fix: flatten keys
jsconst params = new URLSearchParams({ "filter.status": "open", "filter.assignee": "me" });
Or use a library like qs to handle nested query encoding:
jsimport qs from "qs"; const q = qs.stringify({ filter: { status: "open" } }); // filter%5Bstatus%5D=open
5) Error messages: throwing or concatenating objects
Problem
jsthrow new Error("Validation failed: " + errors);
If errors is an object, you get:
Validation failed: [object Object]
Fix
- Prefer attaching details:
jsconst err = new Error("Validation failed"); err.details = errors; throw err;
- Or stringify:
jsthrow new Error(`Validation failed: ${JSON.stringify(errors)}`);
In many systems, structured errors are better than long strings because they’re searchable and parseable in logs.
Debugging workflow: how to find where coercion happens
When [object Object] appears, the real question is: what code path is turning an object into a string?
1) Grep for concatenation and template literals
Look for patterns:
"..." + something`${something}`.textContent = something.innerHTML = somethingURLSearchParamscreation
In large codebases, ripgrep patterns are effective:
bashrg "\+\s*\w+" src rg "\$\{[^}]+\}" src rg "textContent\s*=" src
2) Add type-aware logging
Instead of:
jsconsole.log("value=" + value);
Use:
jsconsole.log({ value, type: typeof value, isArray: Array.isArray(value) });
Or in Node:
jsconsole.log(require("node:util").inspect(value, { depth: 4 }));
3) Use breakpoints on setters (browser devtools)
If [object Object] shows in the DOM:
- Inspect the element
- In Chrome DevTools: Break on → Subtree modifications
- Trigger the behavior
- Devtools will pause where DOM was changed
Then you can see exactly what value is being assigned.
4) Add runtime guards and assertions
In TypeScript or runtime JS, assert string expectations:
tsfunction expectString(x: unknown, label: string): asserts x is string { if (typeof x !== "string") { throw new TypeError(`${label} must be a string, got ${Object.prototype.toString.call(x)}`); } } expectString(title, "title");
This shifts the bug earlier and makes the source obvious.
Best practices for avoiding [object Object] in production
1) Use TypeScript (or strong runtime validation)
TypeScript catches many “object used as string” mistakes at compile time.
Example:
tsconst el = document.querySelector("#out")!; const user: { name: string } = { name: "Asha" }; // @ts-expect-error: Type '{ name: string; }' is not assignable to type 'string'. el.textContent = user; el.textContent = user.name; // OK
For runtime boundaries (incoming HTTP requests, environment variables), use schema validation (Zod, Joi, Valibot). That prevents unknown objects from leaking into string contexts.
2) Prefer structured logging APIs
Tools like pino, winston, bunyan, and many APM loggers support object fields natively.
- Structured logs are searchable by fields.
- You avoid accidental concatenation.
- They handle serialization more predictably.
Comparison notes:
- pino: very fast, JSON-first, great for production.
- winston: flexible transports, a bit heavier, common in older codebases.
- console.log: fine locally, inconsistent in production aggregators.
A good practice:
jslogger.info({ userId, orgId, payload }, "incoming payload");
Instead of embedding objects into strings.
3) Decide on a consistent “stringification policy”
Different use cases want different representations:
- Human readable debug:
util.inspect(value, { depth }) - Data interchange:
JSON.stringifywith schema-safe objects - UI display: explicitly select fields and format them
Avoid sprinkling JSON.stringify everywhere without thinking. It can:
- explode log volume
- leak PII
- break on circular references
- hide the real bug (wrong type)
4) Implement toString() or toJSON() for domain objects (carefully)
If you have domain classes, a custom toString can be useful:
jsclass Money { constructor(amount, currency) { this.amount = amount; this.currency = currency; } toString() { return `${this.currency} ${this.amount.toFixed(2)}`; } } const price = new Money(12.5, "USD"); String(price); // "USD 12.50"
For JSON serialization, toJSON controls output:
jsclass User { constructor(id, email) { this.id = id; this.email = email; } toJSON() { return { id: this.id }; } } JSON.stringify(new User("u1", "secret@example.com")); // {"id":"u1"}
Be cautious: overriding serialization can surprise other engineers. Document it and keep it consistent.
5) Avoid implicit coercion (+ with mixed types)
Instead of:
js"count=" + count
Prefer:
js`count=${Number(count)}`
And if the value can be object-shaped, validate first.
Deep dive: JavaScript coercion rules that lead to [object Object]
When you do string concatenation, JS will run ToPrimitive then ToString.
For objects, ToPrimitive tries (in order):
obj[Symbol.toPrimitive](hint)if presentvalueOf()toString()
For a normal object:
valueOf()returns the object itself (not primitive)toString()returns"[object Object]"
That’s why you get the default.
Customizing via Symbol.toPrimitive
You can precisely control coercion:
jsconst user = { name: "Asha", [Symbol.toPrimitive](hint) { if (hint === "string") return this.name; return null; } }; String(user); // "Asha" `${user}`; // "Asha"
This can be powerful for value objects, but use sparingly in application-level data structures because it can hide bugs.
Practical examples by environment
Node.js + Express: [object Object] in responses
Problem
jsapp.get("/debug", (req, res) => { res.send({ ok: true }); });
This is actually fine: Express detects objects and responds with JSON.
But this is not:
jsapp.get("/debug", (req, res) => { res.send("result=" + { ok: true }); }); // "result=[object Object]"
Fix
jsres.json({ ok: true }); // or res.send(`result=${JSON.stringify({ ok: true })}`);
Fetch API: sending objects incorrectly
Problem: query string
jsfetch(`/api/items?filter=${{ status: "open" }}`); // /api/items?filter=[object Object]
Fix:
jsfetch(`/api/items?filter=${encodeURIComponent(JSON.stringify({ status: "open" }))}`);
Problem: body without JSON headers
jsfetch("/api/items", { method: "POST", body: { status: "open" } });
The body must be a string/Buffer/Blob/FormData/etc.
Fix:
jsfetch("/api/items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "open" }) });
HTML forms: hidden input values
Problem
html<input type="hidden" id="payload" name="payload" /> <script> document.querySelector('#payload').value = { a: 1 }; // value becomes "[object Object]" </script>
Fix:
jsdocument.querySelector('#payload').value = JSON.stringify({ a: 1 });
Server-side, decode and parse JSON.
Safe formatting patterns you can standardize
Pattern 1: formatValue utility for logs
jsimport util from "node:util"; export function formatValue(value) { if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean" || value == null) { return String(value); } return util.inspect(value, { depth: 4, breakLength: 120 }); }
Use:
jslogger.debug(`payload=${formatValue(payload)}`);
Pattern 2: enforce “no string concatenation with unknowns” in lint
ESLint can help reduce accidental coercion:
- Prefer template literals? That still coerces.
- Better: disallow concatenation with non-literals in certain contexts.
You can also add rules like:
@typescript-eslint/restrict-plus-operands@typescript-eslint/restrict-template-expressions
Example (TypeScript ESLint):
json{ "rules": { "@typescript-eslint/restrict-plus-operands": "error", "@typescript-eslint/restrict-template-expressions": [ "error", { "allowNumber": true, "allowBoolean": true, "allowNullish": false } ] } }
This forces engineers to explicitly handle object formatting.
Security and privacy considerations
A common “fix” is to JSON.stringify everything. That can introduce new risks:
- PII leakage: user emails, tokens, addresses dumped into logs
- Credential leakage:
Authorizationheaders, cookies - Log injection: untrusted values breaking log formats
Best practices:
- Redact sensitive fields in log serializers
- Log IDs and metadata, not entire payloads
- Use allowlists for what gets rendered in UI
Example redaction with pino:
jsimport pino from "pino"; const logger = pino({ redact: { paths: ["req.headers.authorization", "password", "token"], remove: true } });
Checklist: when you see [object Object], do this
- Find the rendering/logging site where the string appears.
- Search for coercion:
+, template literals, DOM setters, query/form encoding. - Inspect the actual value with
console.log(value)(not concatenated) orutil.inspect. - Decide intent:
- show a property (
user.name) - show a summary (
User#123) - serialize for transport (
JSON.stringify) - log structured fields
- show a property (
- Add guards (TypeScript, runtime assertions, schema validation).
- Prevent recurrence via ESLint rules and logging standards.
Closing thoughts
[object Object] is JavaScript doing exactly what it’s specified to do: providing a default string form when an object ends up in a string context. The real engineering work is ensuring objects don’t get coerced accidentally—and when you do need text, choosing an explicit, safe, and useful representation.
For junior developers, the key habit is: don’t concatenate objects with strings; always pick the fields you want or stringify intentionally.
For senior engineers, the leverage comes from: structured logging, type/lint enforcement, and consistent serialization policies that keep production systems observable without leaking data or hiding bugs.
![Decoding “[object Object]”: A Practical Guide to Debugging JavaScript Object Stringification Across Frontend, Backend, and Tooling](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_876359ed5d.png)