Why “[object Object]” keeps showing up
If you’ve built JavaScript applications long enough—front-end, Node.js back-end, or both—you’ve likely seen the infamous output:
[object Object]
It pops up in UI labels, logs, URLs, API payloads, alert dialogs, and database entries. It’s not a browser bug or a random glitch. It’s JavaScript telling you: “I tried to turn an object into a string, and this is the default representation.”
For developers, especially when working across layers (React/Vue + Node/Express + databases + HTTP), this string can be a symptom of deeper data-shape issues: wrong serialization, mishandled query params, unexpected object types, a bad template binding, or an implicit type conversion.
This article explains:
- What
[object Object]actually means - Where it commonly occurs (UI, network, storage)
- How to debug the root cause quickly
- Practical fixes and best practices
- Tooling comparisons (console, JSON, util.inspect, serializers)
- Defensive strategies for teams and large codebases
Everything below is geared for software developers and engineers, with examples you can paste into your codebase.
The core cause: implicit string conversion
In JavaScript, when you concatenate a string with an object, or otherwise coerce an object to a string, the runtime calls an internal conversion:
ToPrimitive(input, hint)- If it still has an object, it calls
toString()
For plain objects, the default Object.prototype.toString() returns:
js({}).toString(); // "[object Object]"
That’s why you get [object Object]: it’s the default toString() result for most plain objects.
Here are common implicit coercions that trigger it:
jsconst obj = { a: 1 }; "Value: " + obj; // "Value: [object Object]" `${obj}`; // "[object Object]" String(obj); // "[object Object]" obj + ""; // "[object Object]" // Often in DOM or UI frameworks element.textContent = obj; // sets to "[object Object]" alert(obj); // shows "[object Object]"
This isn’t always wrong—sometimes you intentionally stringify objects in logs, and you just forgot to choose the right formatting.
Common scenarios where it appears
1) UI rendering mistakes (React/Vue/Angular)
React
React will throw for rendering a plain object as a child:
jsxreturn <div>{ { a: 1 } }</div>; // Error: Objects are not valid as a React child
But [object Object] can still appear if you do string concatenation inside JSX:
jsxreturn <div>{"Result: " + result}</div>; // If result is an object => "Result: [object Object]"
Fix: render specific fields, map arrays, or pretty-print JSON.
jsxreturn <div>Result: {result.status}</div>; // or for debugging return <pre>{JSON.stringify(result, null, 2)}</pre>;
Vue
Vue templates often coerce values into strings:
html<p>{{ result }}</p>
If result is an object, Vue uses toString() and you’ll see [object Object].
Fix: bind to a property or use JSON.
html<p>{{ result.status }}</p> <pre>{{ JSON.stringify(result, null, 2) }}</pre>
Angular
Angular tends to display [object Object] when you bind objects without a pipe:
html<div>{{ result }}</div>
Fix: use the json pipe or select fields.
html<pre>{{ result | json }}</pre> <div>{{ result.status }}</div>
2) Logging that hides the real data
The mistake:
jsconsole.log("payload=" + payload);
Instead, do:
jsconsole.log("payload=", payload);
Most consoles (browser DevTools, Node) will render objects interactively if you pass them as separate arguments.
Node.js tip: util.inspect
In Node, console.log(obj) uses util.inspect under the hood, which is often enough. When you need more control:
jsimport util from "node:util"; console.log(util.inspect(payload, { depth: null, colors: true, maxArrayLength: 50, }));
Tool comparison:
console.log("x=", obj)is best for quick devJSON.stringify(obj)is best for producing stable strings for logsutil.inspectis best when objects include non-JSON types (e.g., Map, Set) or circular references
3) Query strings and URLs
A very common source is building URLs incorrectly:
jsconst params = { q: "shoes", filters: { size: 10 } }; const url = `/search?filters=${params.filters}`; // /search?filters=[object Object]
The server receives a string literal [object Object], and now you have a data contract bug.
Fix options:
A) Use URLSearchParams for flat values
jsconst sp = new URLSearchParams({ q: "shoes", size: "10" }); const url = `/search?${sp.toString()}`;
B) JSON encode nested objects
jsconst filters = { size: 10, color: "black" }; const sp = new URLSearchParams({ q: "shoes", filters: JSON.stringify(filters), }); // On the server: const filtersObj = JSON.parse(req.query.filters);
C) Use a robust querystring serializer (recommended)
Libraries like qs support nested objects:
jsimport qs from "qs"; const url = `/search?${qs.stringify({ q: "shoes", filters: { size: 10, color: "black" }, })}`; // /search?q=shoes&filters%5Bsize%5D=10&filters%5Bcolor%5D=black
On the server (Express):
js// With qs-style parsing enabled depending on your stack app.set("query parser", "extended");
Best practice: define a consistent rule for nested params (JSON-in-query vs bracket syntax) across services.
4) FormData and multipart submissions
FormData only stores strings or blobs. If you append an object:
jsconst fd = new FormData(); fd.append("metadata", { a: 1 });
Browsers will coerce it to "[object Object]".
Fix: serialize explicitly.
jsfd.append("metadata", JSON.stringify({ a: 1 }));
On the server, parse it:
jsconst metadata = JSON.parse(req.body.metadata);
If you need typed fields or large nested content, consider sending JSON (application/json) instead of multipart unless files are involved.
5) LocalStorage / sessionStorage
Storage APIs store only strings:
jslocalStorage.setItem("user", user); // stores "[object Object]"
Fix:
jslocalStorage.setItem("user", JSON.stringify(user)); const userObj = JSON.parse(localStorage.getItem("user"));
Caution for seniors: if you deploy migrations for stored data, version your stored payloads:
jslocalStorage.setItem("user.v2", JSON.stringify({ version: 2, data: user }));
6) Database persistence bugs
If you’re persisting a JS object into a text column without serialization, many ORMs or drivers won’t help you.
Example (pseudo):
jsawait db.insert({ payload: someObject });
Depending on driver behavior, you might get [object Object] in the DB.
Fix:
- Prefer JSON/JSONB columns (PostgreSQL)
- Or serialize explicitly
jsawait db.insert({ payload: JSON.stringify(someObject) });
Best practice: enforce DB schema types and add constraints/tests to prevent accidental string coercion.
Debugging: how to find where the coercion happens
When [object Object] appears, the object has already been coerced to a string somewhere. You need to find that conversion point.
Step 1: Search for suspicious patterns
In codebases, grep for:
+concatenation with variables- template literals involving unknown values
.textContent =or.innerHTML =assignmentssetItem(withoutJSON.stringifyFormData.append(
Search strings:
" +`${`setItem(append(
Step 2: Log types and shapes
Before the suspected conversion, log:
jsconsole.log({ type: typeof value, isArray: Array.isArray(value), isNull: value === null, ctor: value?.constructor?.name, value, });
This quickly reveals whether you have:
- An unexpected object instead of string
- A
Date,Map,Set,Error, or class instance - A nested structure you’re assuming is flat
Step 3: Use stack traces by throwing intentionally
If you don’t know where the coercion happens, you can force a stack trace when a value is an object but expected to be string.
jsfunction expectString(x, label = "value") { if (typeof x !== "string") { const err = new Error(`${label} must be string, got ${typeof x}`); err.details = { ctor: x?.constructor?.name, x }; throw err; } return x; } // Use near the boundary const name = expectString(input.name, "input.name");
In production systems, you’d use validation libraries rather than throwing ad hoc, but the technique is effective during debugging.
Step 4: Inspect network payloads
When [object Object] appears in servers:
- Inspect the raw incoming request (body/query)
- Check the
Content-Type - Confirm JSON parsing middleware is configured (
express.json()) and placed correctly
In browsers:
- Use the Network tab to inspect request payloads
- Verify if a field is literally
"[object Object]"
Fix patterns that scale
1) Prefer structured logging over string concatenation
Bad:
jslogger.info("user=" + user);
Good (most loggers support structured fields):
jslogger.info({ userId: user.id, user }, "User loaded");
If using plain console:
jsconsole.log("User loaded", user);
2) Validate inputs at boundaries (APIs, message queues, UI forms)
Using Zod example:
tsimport { z } from "zod"; const SearchQuery = z.object({ q: z.string(), page: z.coerce.number().int().min(1).default(1), filters: z .string() .optional() .transform((s) => (s ? JSON.parse(s) : undefined)), }); const parsed = SearchQuery.parse(req.query);
This prevents implicit conversions from silently leaking. If filters unexpectedly becomes [object Object], JSON.parse will throw and you’ll catch it early.
3) Create explicit serialization helpers
For query params:
jsexport function encodeJsonParam(obj) { return encodeURIComponent(JSON.stringify(obj)); } export function decodeJsonParam(s) { return JSON.parse(decodeURIComponent(s)); }
For storage:
jsexport function setJson(key, value) { localStorage.setItem(key, JSON.stringify(value)); } export function getJson(key) { const raw = localStorage.getItem(key); return raw == null ? null : JSON.parse(raw); }
This avoids copy-pasting JSON.stringify everywhere and reduces mistakes.
4) Handle circular references safely
JSON.stringify fails on cycles:
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
Options:
- Use
structuredCloneto remove cycles (not always possible) - Use libraries like
flattedfor safe JSON-like encoding - Use
util.inspectfor logs
Example with a safe stringify for logs:
jsimport safeStableStringify from "safe-stable-stringify"; logger.debug({ payload: safeStableStringify(obj) });
Advanced: customizing object stringification
Sometimes you want a meaningful string representation instead of [object Object].
Implement toString()
jsclass User { constructor(id, email) { this.id = id; this.email = email; } toString() { return `User(${this.id}, ${this.email})`; } } const u = new User(1, "a@b.com"); "User: " + u; // "User: User(1, a@b.com)"
Implement toJSON() for JSON.stringify
jsclass User { constructor(id, email, passwordHash) { this.id = id; this.email = email; this.passwordHash = passwordHash; } toJSON() { return { id: this.id, email: this.email }; } } JSON.stringify(new User(1, "a@b.com", "...") ); // {"id":1,"email":"a@b.com"}
This is powerful but also risky: it can hide fields unexpectedly. Use it deliberately and document the behavior.
Symbol.toPrimitive for precise coercion
jsclass Money { constructor(cents) { this.cents = cents; } [Symbol.toPrimitive](hint) { if (hint === "number") return this.cents / 100; return `$${(this.cents / 100).toFixed(2)}`; } } const m = new Money(1234); `${m}`; // "$12.34" +m; // 12.34
This can prevent [object Object] in string contexts, but don’t rely on it for data interchange—only for display.
Security and correctness considerations
Don’t log sensitive objects blindly
The “fix” for [object Object] is often JSON.stringify(obj), but that can leak secrets (tokens, passwords, PII) to logs.
Best practices:
- Redact sensitive fields at the logger layer
- Use allowlists rather than blocklists
Example redaction approach:
jsfunction redact(obj) { if (!obj || typeof obj !== "object") return obj; const copy = Array.isArray(obj) ? [...obj] : { ...obj }; for (const key of ["password", "token", "authorization"]) { if (key in copy) copy[key] = "[REDACTED]"; } return copy; } console.log("payload=", redact(payload));
Avoid stuffing complex objects into places meant for strings
Even if you fix [object Object] by JSON-stringifying, question the design:
- Should this value be in the URL?
- Should this be stored in localStorage?
- Should this be passed through query strings?
URLs should remain relatively small and stable; prefer server-side state or resource identifiers.
Testing strategies to prevent regressions
Unit tests for serialization boundaries
For example, tests around URL builders:
jsimport { buildSearchUrl } from "./urls"; test("does not emit [object Object]", () => { const url = buildSearchUrl({ q: "shoes", filters: { size: 10 } }); expect(url).not.toContain("[object Object]"); });
Property-based testing (advanced)
If you have a serializer/deserializer pair, property-based testing can validate round-trip correctness for many random inputs.
Linting rules and code review checklists
Add conventions:
- No
"" + objor"prefix" + valuefor non-primitives - Use structured logging
- Use
URLSearchParams/qsfor query building
ESLint can catch risky patterns (not perfectly, but helpful). You can also add custom lint rules for your team’s patterns.
Quick reference: what to do instead of causing coercion
| Situation | Bad | Better |
|---|---|---|
| Logging | "x=" + obj | console.log("x=", obj) |
| UI display | {obj} / "" + obj | {obj.name} or <pre>{JSON.stringify(obj,null,2)}</pre> |
| localStorage | setItem(key, obj) | setItem(key, JSON.stringify(obj)) |
| query param | ?x=${obj} | qs.stringify or JSON encode |
| FormData | fd.append("x", obj) | fd.append("x", JSON.stringify(obj)) |
| DB text column | insert object | JSON/JSONB or stringify explicitly |
Putting it together: a real-world debugging walkthrough
Imagine you see this in your server logs:
GET /search?filters=[object%20Object]
Step A: confirm where it’s coming from
In the browser Network tab, inspect the request URL. If it contains filters=[object%20Object], the bug is on the client.
Step B: locate URL construction
Search for filters= or the function building the search URL. You find:
jsconst url = `/search?filters=${filters}`;
Step C: check the type
jsconsole.log(typeof filters, filters); // object { size: 10, color: "black" }
Step D: choose a serialization strategy
- If backend expects nested params: use
qs. - If you own both client and server and want simplicity: JSON encode.
Implement:
jsconst sp = new URLSearchParams({ filters: JSON.stringify(filters), }); const url = `/search?${sp}`;
Update server:
jsconst filters = req.query.filters ? JSON.parse(req.query.filters) : {};
Step E: add a regression test
jstest("filters query param is serialized", () => { const url = buildUrl({ filters: { size: 10 } }); expect(decodeURIComponent(url)).toContain("filters={"); });
Now [object Object] won’t sneak back in.
Best practices recap
[object Object]is almost always the result of implicit object-to-string coercion.- Fix the root cause: stop implicitly converting and serialize intentionally.
- Prefer structured logging (
console.log("x=", obj)or logger fields). - Use
JSON.stringifyfor JSON contexts, but be mindful of circular references and sensitive data. - Use
URLSearchParamsfor flat query params; useqsor JSON encoding for nested data. - Validate at boundaries (incoming requests, UI inputs, message payloads) with schema validation.
- Write tests asserting you never emit
[object Object]into URLs, storage, or API payloads.
When you treat serialization as a first-class design concern—rather than a side effect of concatenation—[object Object] stops being a recurring mystery and becomes a quick, easily-caught signal that a boundary needs attention.
![Untangling “[object Object]”: Diagnosing and Fixing JavaScript Object Stringification Bugs in Web Apps and APIs](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_64bb996ca3.png)