Why you keep seeing "[object Object]"
If you’ve built JavaScript applications for any amount of time, you’ve probably seen this unhelpful output somewhere:
- In a browser UI: a React component displays
"[object Object]" - In server logs:
console.log("error: " + err)becomeserror: [object Object] - In an HTTP response: a poorly formatted message returns
{"message":"[object Object]"} - In alerts/toasts:
toast.error(err)prints"[object Object]"
This string is a symptom, not a root cause. It usually means:
- An object got coerced to a string, and
- The environment used the default
Object.prototype.toString()representation.
In JavaScript, string concatenation triggers implicit coercion:
jsconst obj = { a: 1 }; console.log("obj is " + obj); // "obj is [object Object]"
Because obj is not a primitive, JavaScript tries to turn it into a string. If the object doesn’t define a useful string representation, the default is "[object Object]".
Understanding how objects turn into strings (and how to control it) is the key to fixing this class of problems.
The mechanics: object-to-string coercion in JavaScript
When JavaScript needs a string, it will attempt to convert the value using roughly this sequence (simplified):
- If it’s already a string, use it.
- If it’s a number/boolean/bigint/symbol, convert accordingly.
- If it’s an object, attempt
valueOf()and/ortoString(). - If none yields a primitive, throw a
TypeError.
For plain objects, toString() comes from Object.prototype:
js({}).toString(); // "[object Object]"
Template literals don’t save you
Template literals also coerce to string:
jsconst obj = { a: 1 }; console.log(`obj: ${obj}`); // "obj: [object Object]"
console.log(obj) vs "" + obj
In many environments:
console.log(obj)prints an inspectable object structure.console.log("" + obj)prints"[object Object]".
That’s because the console can render objects intelligently when passed as objects, but concatenation forces string coercion before the console sees it.
Common real-world scenarios that produce "[object Object]"
1) Concatenating objects into log lines
jslogger.info("request failed: " + err);
If err is an Error object (or worse, a plain object), the concatenation forces coercion.
Fix: use structured logging or explicit formatting.
jslogger.info({ err }, "request failed"); // or logger.info("request failed: %o", err);
2) Rendering objects in UI frameworks (React/Vue/etc.)
In React:
jsxreturn <div>{someObject}</div>;
React will attempt to render values. Objects are not valid React children unless they’re React elements, so you might see errors—or if you convert implicitly elsewhere, you’ll get "[object Object]".
Fix: render fields or stringify intentionally.
jsxreturn <pre>{JSON.stringify(someObject, null, 2)}</pre>;
For production UI, prefer rendering specific properties rather than dumping JSON.
3) Network/HTTP error handling
A frequent culprit: catching an Axios/fetch error and treating it like a string.
jstry { await axios.get("/api"); } catch (err) { return res.status(500).json({ message: "Failed: " + err }); }
This is especially bad because you lose the real error and ship useless output to clients.
Fix: extract err.message, err.response.data, etc.
jsimport axios from "axios"; function formatAxiosError(err) { if (axios.isAxiosError(err)) { return { message: err.message, status: err.response?.status, data: err.response?.data, }; } return { message: String(err) }; }
4) FormData, URLSearchParams, Map, Set
Many non-plain objects don’t serialize how you expect.
jsconst fd = new FormData(); fd.append("a", "1"); console.log("fd=" + fd); // often "fd=[object FormData]" or "[object Object]" depending on env
You need custom conversion (iterate entries).
jsconst asObject = Object.fromEntries(fd.entries()); console.log(asObject);
5) You stringified the wrong thing
Sometimes the string is literally inside your object:
jsconst message = String({ a: 1 }); // message is "[object Object]"
Then it propagates into storage, metrics, or UI.
First-aid debugging: find where coercion happens
When you see "[object Object]", the key question is: Where did the coercion happen?
Technique 1: Search for concatenation and interpolation
Look for patterns:
"" + value"text" + value`${value}`value.toString()String(value)
In large codebases, ripgrep helps:
bashrg "\+\s*err|\$\{err\}|String\(err\)|toString\(" src
Technique 2: Log types before rendering
jsconsole.log(typeof value, value?.constructor?.name);
Technique 3: Use console.trace() at the suspect site
If you suspect a specific renderer/log formatter:
jsfunction stringifyForDisplay(x) { console.trace("stringifyForDisplay called"); return String(x); }
Technique 4: Enable “pause on exceptions”
In Chrome DevTools:
- Enable Pause on exceptions
- Reproduce the issue
- Inspect call stack around where the string is created
This is extremely effective for UI cases where an error boundary or toast system stringifies unknown values.
The right way to stringify objects (and the tradeoffs)
Option A: JSON.stringify (best default, with caveats)
jsJSON.stringify({ a: 1 }); // '{"a":1}'
Pros:
- Standard
- Works well for plain data
Cons:
- Drops
undefined, functions, symbols - Fails on circular references
- Errors (
Errorobjects) lose non-enumerable fields likemessageandstackunless you handle them
Handling pretty output
jsJSON.stringify(obj, null, 2)
Handling circular references
A simple safe stringifier:
jsfunction safeJsonStringify(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); } if (val instanceof Error) { return { name: val.name, message: val.message, stack: val.stack }; } return val; }); }
This is a pragmatic approach for logs. For UI, you might want more careful redaction.
Option B: Node.js util.inspect (excellent for logs)
In Node:
jsimport util from "node:util"; console.log(util.inspect(obj, { depth: 5, colors: true }));
Pros:
- Handles circular refs
- Great readability
Cons:
- Node-specific
- Output isn’t JSON (harder to parse)
Option C: Use a battle-tested library
Two common choices:
fast-safe-stringify: safe JSON stringify with circular handlingpino(logger) with serializers and structured output
Example with pino:
jsimport pino from "pino"; const logger = pino({ serializers: { err: pino.stdSerializers.err, }, }); try { throw new Error("boom"); } catch (err) { logger.error({ err }, "request failed"); }
This prevents the “stringify error as [object Object]” anti-pattern and preserves stack traces.
Fixing error handling: the biggest source of [object Object]
The fundamental rule
Never assume the caught value is an Error.
In JavaScript, you can throw anything:
jsthrow { code: "E_BAD", detail: { reason: "nope" } };
So in catch (e), e is unknown in TypeScript (or should be).
TypeScript pattern: normalize unknown into a real Error
tsexport function toError(e: unknown): Error { if (e instanceof Error) return e; if (typeof e === "string") return new Error(e); try { return new Error(JSON.stringify(e)); } catch { return new Error(String(e)); } }
Use it like:
tstry { // ... } catch (e: unknown) { const err = toError(e); logger.error({ err }, "operation failed"); throw err; // or map to domain error }
Avoid: "" + err
Even if err is an Error, concatenation can lose key context. Prefer:
err.messageerr.stack- structured log fields
jsconsole.error(err); // retains stack in many consoles
UI rendering patterns: don’t stringify blindly
If a toast/snackbar expects a string but you pass an object, it will coerce.
Bad
tstoast.error(apiError); // apiError is object
Better: extract message safely
tsfunction humanizeError(e: unknown): string { if (e instanceof Error) return e.message; if (typeof e === "string") return e; if (e && typeof e === "object" && "message" in e && typeof (e as any).message === "string") { return (e as any).message; } return "Something went wrong"; } toast.error(humanizeError(apiError));
Best: show users a friendly message, log the details separately
tscatch (e: unknown) { logger.error({ err: toError(e), context: { feature: "checkout" } }, "Checkout failed"); toast.error("Checkout failed. Please try again."); }
This avoids leaking internal object structure into UI while still preserving details in telemetry.
Serialization gotchas that lead to surprising output
Error objects don’t JSON.stringify well by default
jsJSON.stringify(new Error("nope")); // '{}'
Because message and stack are non-enumerable. Solutions:
- Log the
Errorobject directly in consoles - Use logger serializers (
pino.stdSerializers.err) - Provide a custom replacer
Example replacer:
jsfunction errorReplacer(key, value) { if (value instanceof Error) { return { name: value.name, message: value.message, stack: value.stack, ...value, // include enumerable custom props }; } return value; } JSON.stringify({ err: new Error("x") }, errorReplacer, 2);
BigInt breaks JSON
jsJSON.stringify({ n: 1n }); // TypeError: Do not know how to serialize a BigInt
If you stringify unknown values, you may catch exceptions and fall back to String(value)—which can become "[object Object]".
Handle it explicitly:
jsfunction jsonReplacer(_key, value) { if (typeof value === "bigint") return value.toString(); return value; }
Map/Set stringify to {}
jsJSON.stringify(new Map([['a', 1]])); // '{}'
Convert them:
jsconst map = new Map([['a', 1]]); JSON.stringify(Object.fromEntries(map)); // '{"a":1}'
DOM objects and event objects
Attempting to stringify DOM nodes/events often yields noisy or circular structures.
For debugging, log the object directly (not coerced), and pick fields for telemetry.
Best practices: prevent [object Object] in the first place
1) Prefer structured logging over string building
Bad:
jslogger.info("user=" + user + " action=" + action);
Good:
jslogger.info({ userId: user.id, action }, "user action");
Benefits:
- Searchable fields
- Avoids coercion issues
- Better compatibility with log aggregators
2) Make implicit coercion a code smell
Treat these as red flags in code review:
"" + x"something: " + xwherexmay not be primitive- Passing unknown values into APIs that expect strings
3) Use TypeScript’s unknown in catch blocks
Enable useUnknownInCatchVariables in tsconfig.json:
json{ "compilerOptions": { "useUnknownInCatchVariables": true } }
This forces you to consciously handle thrown values and reduces the chance you’ll stringify a random object.
4) Add safe formatters at boundaries
Create “boundary utilities”:
humanizeError(e: unknown): stringfor UItoError(e: unknown): Errorfor logging and rethrowingsafeJsonStringify(value: unknown): stringfor debugging output
Centralizing these prevents ad-hoc String(e) sprinkled everywhere.
5) Implement toString() (or Symbol.toPrimitive) for domain objects sparingly
You can define your own string conversion:
jsclass Money { constructor(amount, currency) { this.amount = amount; this.currency = currency; } toString() { return `${this.currency} ${this.amount.toFixed(2)}`; } } console.log("total=" + new Money(10, "USD")); // "total=USD 10.00"
This can be useful for value objects. But be careful:
- It may leak sensitive info if used in logs
- It may hide issues where you should be logging structured fields
For more control:
jsclass UserId { constructor(value) { this.value = value; } [Symbol.toPrimitive](hint) { return hint === "string" ? this.value : NaN; } }
Use this only when you’re confident implicit conversion is desirable.
Tooling comparisons: logging and inspection options
console.log
Pros:
- Ubiquitous
- Browser console expands objects interactively
Cons:
- Not structured by default
- In Node, formatting differs across runtimes
Tip: use format specifiers in Node and browsers:
jsconsole.log("obj=%o", obj); console.log("obj=%O", obj);
Node util.inspect
Pros:
- Great for server-side debug logs
- Handles circular references
Cons:
- Not JSON
- Not ideal for machine parsing
Structured loggers (Pino, Winston)
Pino
- Very fast
- Great JSON logs, excellent ecosystem
- Strong error serializers
Winston
- Flexible transports
- More configuration overhead
Best practice for production: structured JSON logs with consistent fields (requestId, userId, err, etc.) and centralized serializers.
A practical end-to-end example: from bug to fix
Symptom
Users report toast messages showing: "[object Object]".
Existing code
tsasync function saveProfile(input: ProfileInput) { try { await api.put("/profile", input); toast.success("Saved!"); } catch (e) { toast.error("Save failed: " + e); } }
If api.put throws an Axios error, concatenation stringifies it to "[object Object]".
Debugging
- Set breakpoint in catch.
- Inspect
e:typeof e === "object"ehasresponse,message, etc.
- Confirm coercion site:
"Save failed: " + e.
Improved solution
tsimport axios from "axios"; function extractApiMessage(e: unknown): string { if (axios.isAxiosError(e)) { const data = e.response?.data as any; // common API shape: { message: string } if (data && typeof data.message === "string") return data.message; return e.message; } if (e instanceof Error) return e.message; if (typeof e === "string") return e; return "Request failed"; } async function saveProfile(input: ProfileInput) { try { await api.put("/profile", input); toast.success("Saved!"); } catch (e: unknown) { // user-friendly toast.error(extractApiMessage(e)); // developer-friendly console.error(e); } }
Outcome:
- Users see meaningful messages.
- Developers still get full error objects in console.
- No more
"[object Object]".
When you should show JSON (and how to do it safely)
Sometimes you’re building internal tools, admin panels, or debugging views where raw JSON is helpful.
Use <pre> with safe stringify
tsxfunction DebugPanel({ value }: { value: unknown }) { return ( <pre style={{ whiteSpace: "pre-wrap" }}> {safeJsonStringify(value)} </pre> ); }
Redact secrets
Before dumping objects, consider redaction:
tsconst SECRET_KEYS = new Set(["password", "token", "authorization", "cookie"]); function redact(value: any): any { if (!value || typeof value !== "object") return value; if (Array.isArray(value)) return value.map(redact); const out: any = {}; for (const [k, v] of Object.entries(value)) { out[k] = SECRET_KEYS.has(k.toLowerCase()) ? "[REDACTED]" : redact(v); } return out; } safeJsonStringify(redact(obj));
This is especially important if debug panels can be accessed in production.
Checklist: eliminating [object Object] systematically
- Never concatenate unknown objects into strings for logs/UI.
- Prefer structured logging (
logger.info({ ... }, "msg")). - In TypeScript, treat
catchasunknownand normalize errors. - Use
JSON.stringifyintentionally, and handle:- circular references
ErrorobjectsMap/SetBigInt
- Separate user messaging from developer diagnostics.
- Add lint rules / review guidelines against
"" + xpatterns.
Closing thoughts
"[object Object]" is JavaScript telling you, “You asked me for a string, but you handed me a generic object.” The fix is rarely to “get rid of the message” and almost always to choose the right representation for the right audience:
- For machines and observability: structured fields and JSON
- For developers: inspectable objects, rich error serialization, stack traces
- For users: a safe, friendly message
Once you adopt consistent boundary utilities (toError, humanizeError, safeJsonStringify) and structured logging, this problem largely disappears—and when it does show up, you’ll be able to track down the coercion site quickly and confidently.
![Fixing the “[object Object]” Problem: Root Causes, Debugging Techniques, and Robust Patterns for JavaScript/TypeScript](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_6458bbca0b.png)