Overview
If you’ve ever seen [object Object] show up in your UI, logs, alerts, toasts, or API error messages, you’ve hit one of the most common (and most misunderstood) JavaScript pitfalls: an object is being coerced to a string.
In JavaScript, when an object is used where a string is expected—such as in string concatenation, template contexts, DOM text updates, or certain logging patterns—the runtime calls an internal conversion to string. For plain objects, that often becomes the unhelpful default:
text[object Object]
This article explains:
- What
[object Object]actually means in JavaScript - The most common ways it leaks into user-visible output
- Practical debugging techniques to find the source quickly
- Robust patterns for serialization, logging, and UI rendering
- Framework-specific notes (React, Angular, Vue)
- Best practices for production-grade error handling and observability
The goal is to help junior devs understand why it happens, and to give senior engineers tooling and patterns to prevent it entirely.
1) What [object Object] means
[object Object] is the result of converting a typical JavaScript object to a string using default stringification.
Example:
jsconst obj = { a: 1 }; console.log(String(obj)); // "[object Object]" console.log(obj + ""); // "[object Object]"
Why? When the JS engine needs a string representation of an object, it performs a conversion process (spec-related “ToPrimitive” then “ToString”). For a plain object, the default Object.prototype.toString() returns:
js({}).toString(); // "[object Object]"
That output is not meant for user-facing display; it’s essentially a fallback.
Key takeaway
Whenever you see [object Object], it almost always means:
- You intended to show a specific field (like
user.name) but passed the whole object (user). - Or you intended to serialize it (JSON) but did implicit string coercion.
2) The most common ways [object Object] appears
2.1 String concatenation
jsconst user = { id: 1, name: "Ava" }; const message = "Hello " + user; // "Hello [object Object]"
Fix:
jsconst message = `Hello ${user.name}`;
If you truly want the full object:
jsconst message = `Hello ${JSON.stringify(user)}`;
2.2 Template literals with objects
Template literals still perform string conversion:
js`User: ${user}`; // "User: [object Object]"
Fix with a property or explicit serializer.
2.3 DOM APIs: textContent, innerText, innerHTML
jsel.textContent = user; // coerces to string => [object Object]
Fix:
jsel.textContent = user.name;
2.4 alert() and browser dialogs
jsalert({ error: "Bad" }); // [object Object]
Fix:
jsalert(JSON.stringify({ error: "Bad" }, null, 2));
2.5 Logging mistakes
Some logging patterns produce [object Object] depending on environment:
jsconsole.log("User=" + user); // User=[object Object]
Better:
jsconsole.log("User=", user); // shows expandable object in devtools
Or:
jsconsole.log("User=%o", user); // formatted object (browser)
2.6 Error messages from APIs
Many HTTP client stacks throw structured objects, not strings.
Example with Axios:
jstry { await axios.get("/api"); } catch (err) { toast.error("Request failed: " + err); }
This can become [object Object] because err is an object. Instead extract useful fields:
jscatch (err) { const message = err.response?.data?.message ?? err.message ?? "Unknown error"; toast.error(`Request failed: ${message}`); }
3) Root cause analysis: where coercion happens
[object Object] shows up when an object is forced into a string context. Common “string contexts”:
- Using
+with a string on either side - Template literal interpolation:
${value} - Assigning to DOM text fields (
textContent,innerText, oftenvalue) - Passing to APIs expecting strings (
alert, some UI libraries, some logger transports) - Throwing non-Error objects and later concatenating
A mental model
If you ask, “Why did it stringify?” look for code that:
- Builds a string
- Renders text
- Serializes output
- Formats errors
Search for the output’s surrounding text (e.g., "Hello", "Error", "User="). Usually the concatenation is nearby.
4) Debugging techniques (fast and reliable)
4.1 Find where the bad string was created
If [object Object] appears in the UI, it came from a string somewhere.
Approach: locate the rendering call.
- If it’s a toast library, set a breakpoint where you call
toast.error/notify. - If it’s React/Vue/Angular, inspect the component props/state being rendered.
4.2 Use “Pause on exceptions” and stack traces
If it appears in logs due to an error, trigger the error and look at the stack trace.
In Chrome DevTools:
- Sources → enable Pause on exceptions
- Reproduce the issue
- Inspect local variables
4.3 Instrument with type-aware logging
Instead of concatenating, log values with types:
jsfunction debugValue(label, v) { console.log(label, { type: typeof v, isArray: Array.isArray(v), ctor: v?.constructor?.name, value: v, }); } debugValue("user", user);
This avoids accidental coercion and makes it obvious you’re dealing with an object.
4.4 Use console.table for arrays/records
jsconsole.table(users);
4.5 Grep for suspicious patterns
Search the codebase for:
+ err+ error+ response`${err}`"" +(explicit coercion)
In a large repo, these patterns quickly find the offenders.
5) Correct solutions: pick the right representation
Not every object should be converted to JSON. Choose representation based on your goal.
5.1 For UI: render a specific field
User-facing text should be deliberate.
js// Bad setStatus(`Logged in as ${user}`); // Good setStatus(`Logged in as ${user.name}`);
When you don’t know the shape (e.g., generic UI component), create an explicit formatter:
jsfunction displayName(user) { return user?.name || user?.email || `User#${user?.id ?? "?"}`; }
5.2 For debugging: console.log(value) not "" + value
Prefer structured logs:
jsconsole.log({ user, requestId, status });
5.3 For sending data over the network: JSON.stringify
jsconst payload = JSON.stringify(obj);
But be mindful of:
- Circular references
- Big objects (performance, bandwidth)
- Sensitive fields
Circular reference-safe stringification
A common production failure is:
jsJSON.stringify(obj); // TypeError: Converting circular structure to JSON
Use a safe serializer:
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); } return val; }); }
Or a library:
- flatted (handles circular)
- safe-stable-stringify (circular + stable order)
5.4 For logs in production: use structured logging
Instead of building strings:
jslogger.info("request_failed", { err, url, status });
Then your logging backend (Datadog, ELK, Loki, CloudWatch) can index fields.
6) Framework-specific scenarios
6.1 React: “Objects are not valid as a React child” vs [object Object]
React often throws if you try to render an object directly:
jsxreturn <div>{user}</div>; // typically throws
But you can still get [object Object] in places like:
- string concatenation
- attributes
jsxreturn <div title={"User: " + user}>Hover</div>;
Fix:
jsxreturn <div title={`User: ${user.name}`}>Hover</div>;
If you want a JSON preview (debug only):
jsx<pre>{JSON.stringify(user, null, 2)}</pre>
6.2 Vue: interpolation can coerce
Vue templates interpolate values:
html<div>{{ user }}</div>
Depending on Vue version and runtime behavior, you may see [object Object].
Fix:
html<div>{{ user.name }}</div>
Or pretty-print (debug):
html<pre>{{ JSON.stringify(user, null, 2) }}</pre>
6.3 Angular: interpolation uses toString()
Angular template interpolation:
html<div>{{ user }}</div>
Will often yield [object Object].
Fix:
html<div>{{ user.name }}</div>
For debugging you can use json pipe:
html<pre>{{ user | json }}</pre>
7) Error handling best practices to prevent [object Object]
Errors are a top source of this issue because many libraries throw rich objects.
7.1 Normalize errors at system boundaries
Create a small utility to convert unknown thrown values into a string + metadata.
ts// TypeScript recommended export type NormalizedError = { message: string; name?: string; stack?: string; cause?: unknown; details?: unknown; }; export function normalizeError(err: unknown): NormalizedError { if (err instanceof Error) { return { name: err.name, message: err.message, stack: err.stack, cause: (err as any).cause, }; } if (typeof err === "string") { return { message: err }; } // Handle common HTTP client shapes const anyErr = err as any; const message = anyErr?.response?.data?.message ?? anyErr?.message ?? "Unknown error"; return { message, details: err, }; }
Use it consistently:
tscatch (err) { const ne = normalizeError(err); toast.error(ne.message); logger.error("request_failed", ne); }
7.2 Never build error strings with + err
Avoid:
jsthrow new Error("Failed: " + err);
Instead:
jsconst ne = normalizeError(err); throw new Error(`Failed: ${ne.message}`);
Or preserve the original error as cause (Node 16+/modern browsers):
jsthrow new Error("Request failed", { cause: err });
7.3 Prefer Error objects over throwing plain objects
Throwing plain objects is legal but causes inconsistent behavior and logging:
jsthrow { message: "Bad" }; // avoid
Prefer:
jsthrow new Error("Bad");
Or custom errors:
tsclass HttpError extends Error { constructor(public status: number, message: string, public details?: unknown) { super(message); this.name = "HttpError"; } }
8) Tooling and library comparisons
8.1 JSON.stringify vs “pretty printing” vs custom formatting
JSON.stringify(obj)is best for network payloads and basic debugging.JSON.stringify(obj, null, 2)is readable for developer-facing output.- Custom formatting is best for user-facing messages.
8.2 Safe serializers
If you frequently serialize arbitrary objects (errors, request contexts):
- safe-stable-stringify: stable key order + handles circular; good for deterministic logs.
- fast-safe-stringify: optimized for performance.
- flatted: handles circular by encoding references (not standard JSON).
8.3 Logging frameworks
If [object Object] appears in production logs, a structured logger prevents it.
- pino (Node): fast JSON logs, great ecosystem.
- winston: flexible transports, more overhead.
- bunyan: older but structured.
Key property: ability to log objects as objects, not as concatenated strings.
9) TypeScript strategies to catch it earlier
TypeScript can reduce [object Object] by forcing you to be explicit.
9.1 Avoid any at boundaries
When everything is any, you’ll concatenate objects by mistake.
Define types for API responses:
tstype User = { id: string; name: string; email: string }; function greet(user: User) { return `Hello ${user.name}`; }
9.2 Use ESLint rules to catch suspicious concatenation
Useful rules/config:
@typescript-eslint/restrict-plus-operands@typescript-eslint/no-base-to-string
no-base-to-string specifically warns when you implicitly call toString() on objects in string contexts.
Example ESLint config snippet:
json{ "rules": { "@typescript-eslint/no-base-to-string": "error", "@typescript-eslint/restrict-plus-operands": "error" } }
This is one of the best preventative measures.
10) UI patterns to prevent accidental object rendering
10.1 Use view models / DTOs
Instead of passing raw API objects into UI components, map them.
tstype UserDto = { id: string; name: string }; function toUserDto(apiUser: any): UserDto { return { id: String(apiUser.id), name: apiUser.name ?? "(unknown)", }; }
Then components render predictable shapes.
10.2 Defensive rendering helpers
tsfunction asText(value: unknown): string { if (value == null) return ""; if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); if (value instanceof Date) return value.toISOString(); return "[Unsupported value]"; }
Use it at boundaries where data is uncertain.
10.3 Never show raw objects to end users
Even if you fix [object Object] with JSON, dumping JSON in user-facing toasts is rarely good UX.
Better pattern:
- Show a friendly message to the user.
- Log detailed structured context for developers.
Example:
tscatch (err) { const ne = normalizeError(err); toast.error("Something went wrong. Please try again."); logger.error("checkout_failed", { ...ne, cartId }); }
11) Practical checklist
When you encounter [object Object]:
- Find the string context (concatenation, template string, DOM assignment, toast message).
- Inspect the value being coerced:
console.log(value)(not"" + value). - Decide intent:
- User display → select specific property/format.
- Debugging → log object directly or pretty JSON.
- Network →
JSON.stringify(safe if needed).
- Add guardrails:
- TypeScript types at boundaries.
- ESLint
no-base-to-stringandrestrict-plus-operands. - Centralized
normalizeError. - Structured logging.
12) Examples: before/after fixes
Example A: Toast shows [object Object]
Before:
jscatch (err) { toast.error("Login failed: " + err); }
After:
jscatch (err) { const message = err?.response?.data?.message ?? err?.message ?? "Unknown error"; toast.error(`Login failed: ${message}`); }
Example B: UI header renders [object Object]
Before:
jsheaderEl.textContent = currentProject;
After:
jsheaderEl.textContent = currentProject.title;
Example C: Debug log useless
Before:
jsconsole.log("payload=" + payload);
After:
jsconsole.log("payload=", payload);
Or if you need a string:
jsconsole.log("payload=\n" + JSON.stringify(payload, null, 2));
Conclusion
[object Object] is a symptom, not the disease. It indicates that an object ended up in a string context without an explicit formatting decision. Fixing it isn’t just about replacing it with JSON.stringify everywhere—though that can help during debugging—it’s about choosing the right representation for the situation:
- User-facing UI: pick meaningful fields and formats.
- Developer debugging: log objects as objects.
- Production observability: use structured logging and error normalization.
- Prevention: TypeScript + ESLint rules to stop implicit object-to-string coercion.
With these practices in place, [object Object] stops being a recurring annoyance and becomes a rare, quickly diagnosed edge case.
![How to Fix “[object Object]” in Web Apps: Root Causes, Debugging, and Best Practices for Developers](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_eb15827139.png)