Clarifying “[object Object]”: How to Avoid, Debug, and Fix Object Stringification Bugs in JavaScript
If you’ve spent any time building JavaScript applications—front-end, Node.js services, or full-stack—you’ve likely seen the mysterious string:
[object Object]
It shows up in logs, UI text, query strings, alert dialogs, HTML attributes, and even API payloads. It’s not an error message by itself; it’s JavaScript doing exactly what you asked—just not what you meant.
This article explains:
- What “
[object Object]” actually means - Why it appears (common triggers)
- How to debug it systematically
- How to fix it safely (with code)
- Best practices for logging, serialization, and rendering objects
- Tooling and framework-specific tips (React, Angular, Vue, Node)
The audience is software developers and engineers, so we’ll cover both junior-friendly explanations and senior-level edge cases.
1) What “[object Object]” actually is
In JavaScript, when you try to use an object in a context that expects a string—like concatenation, templating, DOM text, or implicit coercion—JavaScript converts the object to a primitive.
For a plain object ({}), the default conversion results in:
"[object Object]"
This comes from Object.prototype.toString():
js({}).toString(); // "[object Object]"
The pattern is generally:
"[object " + <internal class> + "]"
For example:
jsObject.prototype.toString.call([]); // "[object Array]" Object.prototype.toString.call(new Date()); // "[object Date]" Object.prototype.toString.call(/re/); // "[object RegExp]"
So “[object Object]” is usually the result of a plain object being stringified implicitly.
2) The most common causes
2.1 String concatenation with an object
jsconst user = { id: 1, name: "Ada" }; console.log("User: " + user); // User: [object Object]
Because + with a string operand triggers string coercion.
2.2 Template literals with an object
jsconsole.log(`User: ${user}`); // User: [object Object]
Template literals call ToString() on the substituted expression.
2.3 Rendering an object in the DOM/UI directly
jsdocument.getElementById("output").textContent = user; // output becomes "[object Object]"
In frameworks, similar things happen:
- React:
{user}in JSX (whenuseris a plain object) - Angular:
{{ user }} - Vue:
{{ user }}
2.4 Passing an object to functions expecting a string
jsalert(user); // [object Object]
Or setting attributes:
jsel.setAttribute("data-user", user); // coerces to string
2.5 Query string / URL building
jsconst url = "/search?q=" + { term: "kafka" }; // "/search?q=[object Object]"
If you intended to encode parameters, you need a query builder.
2.6 Logging systems that flatten values incorrectly
Some log shippers or poorly configured formatters will stringify objects with String(obj) rather than preserving structured JSON.
3) Debugging: finding where coercion happens
“[object Object]” is a symptom. The root cause is usually a value flowing into a string context unexpectedly.
3.1 Start from where the string appears
- UI: inspect the element and search the code for the binding
- Logs: locate the log statement that emitted it
- URLs: inspect network requests / server access logs
Then determine what value was supposed to be there, and why an object reached that point.
3.2 Use typeof and Object.prototype.toString.call
jsfunction typeInfo(x) { return { typeof: typeof x, tag: Object.prototype.toString.call(x), isArray: Array.isArray(x), }; } console.log(typeInfo(user)); // { typeof: 'object', tag: '[object Object]', isArray: false }
3.3 Add targeted assertions (especially in TypeScript)
Even in plain JS, simple runtime guards help:
jsfunction assertString(x, name = "value") { if (typeof x !== "string") { throw new TypeError(`${name} must be a string, got ${Object.prototype.toString.call(x)}`); } } assertString(user, "user"); // throws
In TypeScript, let the compiler prevent this category of bug:
tsfunction greet(name: string) { return `Hello ${name}`; } const user = { name: "Ada" }; // greet(user); // Type error
3.4 Set breakpoints on the problematic rendering/logging code
In Chrome DevTools:
- Find the line setting
textContent/ rendering - Set a breakpoint
- Inspect the variable that is being rendered
- Use “Call Stack” to see where the object originated
For Node.js:
- Run with
node --inspectand use Chrome DevTools - Or add
debugger;temporarily
3.5 Track down implicit coercion with defensive linting
ESLint can prevent many accidental coercions.
Useful rules/configurations:
@typescript-eslint/restrict-plus-operands(TS)no-implicit-coercioneqeqeq(not directly about stringifying, but related coercion)
Example .eslintrc snippet:
json{ "rules": { "no-implicit-coercion": ["error", { "string": true }] } }
4) Fixes: converting objects to strings intentionally
4.1 Use JSON.stringify for readable output
jsconsole.log("User:", JSON.stringify(user));
For pretty printing:
jsconsole.log(JSON.stringify(user, null, 2));
Caveats:
- Circular references throw
- Functions and
undefinedare dropped - BigInt throws unless handled
4.2 Prefer structured logging over string concatenation
Instead of:
jslogger.info("User: " + user);
Do:
jslogger.info({ user }, "User loaded");
This works best with loggers like pino or winston configured for JSON output.
Tool comparison (quick, practical)
- pino: very fast, JSON-first, great for production structured logs
- winston: flexible transports, common in older codebases, more overhead
- bunyan: structured logging pioneer, less common today
If you use log aggregation (ELK, Datadog, OpenTelemetry), structured logs are far more searchable.
4.3 In the UI, render a specific field (not the whole object)
If you meant the name:
jsx// React <div>{user.name}</div>
Angular:
html<div>{{ user.name }}</div>
Vue:
html<div>{{ user.name }}</div>
If you want a debug view:
jsx<pre>{JSON.stringify(user, null, 2)}</pre>
4.4 For URLs and query strings, use URL and URLSearchParams
jsconst url = new URL("https://example.com/search"); url.searchParams.set("term", "kafka"); url.searchParams.set("page", String(2)); console.log(url.toString());
If you need to encode a whole object in a query parameter (sometimes used for state sharing), do it explicitly:
jsurl.searchParams.set("filters", JSON.stringify({ tags: ["js", "node"] }));
On the server, parse it consciously and validate.
4.5 For form data, don’t append plain objects
This is a common hidden source of “[object Object]”:
jsconst fd = new FormData(); fd.append("user", user); // coerces to string
Fix:
jsfd.append("user", JSON.stringify(user));
Or append fields individually:
jsfd.append("userId", String(user.id)); fd.append("userName", user.name);
5) Harder cases: circular references, BigInt, Dates, custom serialization
5.1 Circular structures
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
Solutions:
Option A: Use a safe stringifier
safe-stable-stringifyfast-safe-stringifyflatted
Example:
jsimport safeStringify from "safe-stable-stringify"; console.log(safeStringify(a));
Option B: Implement a replacer to skip cycles
jsfunction jsonStringifySafe(obj) { const seen = new WeakSet(); return JSON.stringify(obj, (key, value) => { if (typeof value === "object" && value !== null) { if (seen.has(value)) return "[Circular]"; seen.add(value); } return value; }); }
5.2 BigInt
jsJSON.stringify({ n: 1n }); // TypeError: Do not know how to serialize a BigInt
Use a replacer:
jsJSON.stringify({ n: 1n }, (k, v) => (typeof v === "bigint" ? v.toString() : v));
5.3 Dates
Dates stringify to ISO automatically with JSON:
jsJSON.stringify({ d: new Date("2020-01-01") }); // {"d":"2020-01-01T00:00:00.000Z"}
But if you’re not using JSON stringify and rely on String(date), you may get locale-specific output. Prefer ISO strings for interoperability.
5.4 Custom object stringification with toString() and Symbol.toPrimitive
You can make objects stringify nicely:
jsclass User { constructor(id, name) { this.id = id; this.name = name; } toString() { return `User(${this.id}, ${this.name})`; } } const u = new User(1, "Ada"); console.log("User: " + u); // User: User(1, Ada)
For more control:
jsclass Money { constructor(cents) { this.cents = cents; } [Symbol.toPrimitive](hint) { if (hint === "number") return this.cents / 100; return `$${(this.cents / 100).toFixed(2)}`; } } const price = new Money(1234); console.log(String(price)); // $12.34 console.log(+price); // 12.34
Best practice: reserve these for domain objects where stringification is meaningful. Don’t use it to hide bugs in logging/UI.
6) Framework-specific pitfalls and fixes
6.1 React
Problem: rendering a plain object
jsxreturn <div>{user}</div>; // [object Object]
Fix: pick fields or render JSON in <pre>:
jsxreturn <div>{user.name}</div>; // or return <pre>{JSON.stringify(user, null, 2)}</pre>;
Problem: accidentally passing an object to a DOM attribute
jsx<div data-user={user} />
React will coerce it for attributes (and may warn in some cases). Prefer:
jsx<div data-user={JSON.stringify(user)} />
Or keep non-string data out of DOM attributes and store it in state/closures.
6.2 Angular
Angular interpolation {{ value }} will call toString() for objects.
Use the built-in json pipe for debugging:
html<pre>{{ user | json }}</pre>
And for production UI, render specific properties.
6.3 Vue
Same concept: {{ user }} will show [object Object].
Use:
html<pre>{{ JSON.stringify(user, null, 2) }}</pre>
Or create a computed value that formats cleanly.
6.4 Node.js + Express/Koa
Problem: sending non-JSON string accidentally
jsres.send("User: " + user); // User: [object Object]
Fix:
jsres.json(user);
Or:
jsres.type("text/plain").send(`User: ${JSON.stringify(user)}`);
7) Best practices to prevent “[object Object]” from shipping
7.1 Prefer explicit formatting at boundaries
The bug happens at boundaries:
- UI rendering
- Logs
- HTTP requests/responses
- Storage (localStorage, DB, cache)
At each boundary, convert intentionally:
- UI: render fields or a formatter
- Logs: structured logger
- HTTP:
res.json()andfetch(..., { body: JSON.stringify(...) }) - Storage: store JSON with versioning
7.2 Use TypeScript (or at least JSDoc) to constrain types
Even a light touch helps:
js/** @param {string} name */ function greet(name) { return `Hello ${name}`; }
With TS, you’ll catch many of these at compile time.
7.3 Adopt a standard logger pattern
In Node:
js// pino example import pino from "pino"; const logger = pino(); logger.info({ userId: user.id, user }, "loaded user");
Avoid "..." + obj in production logs.
7.4 Lint rules and code review heuristics
Heuristics that catch issues early:
- Search for string concatenation with suspicious values
- Avoid
setAttributewith non-string values - Prefer
URLSearchParams - For UI bindings, avoid
{{ obj }}or{obj}unless intentionally stringified
7.5 Add tests for rendered output and payloads
Example (Jest):
jstest("renders user name", () => { const user = { name: "Ada" }; const text = `User: ${user.name}`; expect(text).toBe("User: Ada"); });
Snapshot tests can catch accidental regressions where an object slips into the UI.
8) A systematic checklist when you see “[object Object]”
- Identify the output channel (UI, log, URL, payload)
- Locate the source expression that produced the string
- Inspect the value right before output (
console.log(value, typeof value, value))—note thatconsole.logcan show structure even if later coerced - Find the boundary that coerces to string (
+, template literal, DOM attribute, interpolation) - Fix by formatting intentionally:
- pick a field:
user.name - serialize:
JSON.stringify(user) - structured log:
logger.info({ user }) - proper query encoding:
URLSearchParams
- pick a field:
- Guard it with types or runtime assertions
- Add a regression test
9) Example: tracing a real bug end-to-end
Imagine you see a broken UI label:
"Assigned to: [object Object]"
Code:
jsxfunction TicketRow({ ticket }) { return ( <div> Assigned to: {ticket.assignee} </div> ); }
ticket.assignee is an object: { id, name, email }. React renders it by calling toString().
Fix:
jsxfunction TicketRow({ ticket }) { return ( <div> Assigned to: {ticket.assignee?.name ?? "Unassigned"} </div> ); }
If you also want a hover tooltip:
jsx<div title={ticket.assignee ? JSON.stringify(ticket.assignee) : ""}> Assigned to: {ticket.assignee?.name ?? "Unassigned"} </div>
But generally, don’t dump JSON into attributes for production UX—use a proper tooltip component.
10) Conclusion
“[object Object]” isn’t a mysterious runtime error—it’s a clear sign of implicit object-to-string coercion. The fix is almost always to:
- Render the right field
- Serialize intentionally (often with
JSON.stringify) - Use structured logging
- Avoid concatenating or interpolating raw objects
Treat the appearance of “[object Object]” as a boundary bug: a value crossed from a structured domain into a string channel without an explicit formatting decision.
One clarification
Your prompt lists the topic as [object Object]. If you intended a different topic (for example, an object with a topic field that didn’t serialize correctly), paste the intended topic and I’ll rewrite the article accordingly while keeping the same JSON output format.
![Clarifying “[object Object]”: How to Avoid, Debug, and Fix Object Stringification Bugs in JavaScript](https://trouvai-blog-media.s3.us-east-2.amazonaws.com/data_8eb6c701be.png)