In JavaScript, seeing "[object Object]" appear in a UI, log, alert dialog, URL, or API payload is a classic symptom of a specific class of bugs: something tried to turn an object into a string, and JavaScript fell back to its default object stringification.
This article explains why it happens, how to debug it quickly across browser and Node.js environments, and how to prevent it with robust serialization, type discipline, and observability practices. It’s aimed at software developers and engineers working in modern JavaScript/TypeScript stacks (React/Vue/Angular, Node.js services, serverless, and tooling).
1) What does "[object Object]" actually mean?
In JavaScript, when an object is coerced to a string (implicitly or explicitly), it typically calls a conversion pathway similar to:
obj.toString()(historically)obj[Symbol.toPrimitive]if definedvalueOf()depending on context
For a plain object ({}), the default Object.prototype.toString() returns:
js({}).toString(); // "[object Object]"
So when you see [object Object], it means:
- You had an object value, not a string.
- Something (string concatenation, template literal, DOM API, etc.) attempted to turn it into a string.
- JavaScript used the default representation.
This is not “random.” It’s usually deterministic and fixable.
2) The most common ways you accidentally produce [object Object]
2.1 String concatenation
jsconst user = { id: 123, name: "Ava" }; console.log("User: " + user); // User: [object Object]
Why: "User: " + user forces user to be a string.
Fix: Use structured logging or serialize:
jsconsole.log("User:", user); console.log(`User: ${JSON.stringify(user)}`);
2.2 Template literals
jsconst payload = { ok: true }; const msg = `Response: ${payload}`; // "Response: [object Object]"
Fix:
jsconst msg = `Response: ${JSON.stringify(payload)}`;
2.3 DOM injection / UI rendering
If you do:
jsel.textContent = someObject;
or in React:
jsxreturn <div>{someObject}</div>;
React will complain or render something odd depending on the case. If you do:
jsxreturn <div>{String(someObject)}</div>;
you’ll explicitly get [object Object].
Fix: Render specific fields or pretty-print JSON in a <pre>:
jsxreturn <pre>{JSON.stringify(someObject, null, 2)}</pre>;
2.4 URL query params
jsconst params = new URLSearchParams({ filter: { status: "open" } }); params.toString(); // "filter=%5Bobject+Object%5D"
Fix: Encode objects explicitly:
jsconst params = new URLSearchParams({ filter: JSON.stringify({ status: "open" }) });
Or use a library that supports nested objects such as qs.
2.5 FormData
jsconst fd = new FormData(); fd.append("metadata", { a: 1 }); // becomes "[object Object]"
Fix:
jsfd.append("metadata", JSON.stringify({ a: 1 }));
2.6 Implicit coercion in arrays and objects
jsconst arr = [{ a: 1 }, { b: 2 }]; String(arr); // "[object Object],[object Object]"
Fix:
jsarr.map(x => JSON.stringify(x)).join(",");
3) A systematic debugging approach
When [object Object] shows up, your goal is to answer:
- Which value became
[object Object]? - Where did the coercion happen?
- Why was an object passed where a string was expected?
3.1 Search for common coercion patterns
In a codebase, search for:
"" +(empty string concatenation)+ somethingwheresomethingmight be an object- Template literals:
`${...}` String(.toString()- DOM writes:
.textContent =,.innerHTML = - Query building:
URLSearchParams, string concatenations into URLs
3.2 Use console logging correctly
Bad (forces coercion):
jsconsole.log("payload=" + payload);
Good (keeps structure):
jsconsole.log("payload=", payload); console.dir(payload, { depth: null });
In Node.js, prefer util.inspect for deep objects:
jsimport util from "node:util"; console.log(util.inspect(payload, { depth: null, colors: true }));
3.3 Add a stack trace at the coercion site
If you find a place that might be doing string coercion but you’re not sure it’s the one, temporarily add:
jsconsole.trace("Object coerced here", value);
This prints the call stack, letting you identify the exact path.
3.4 Break on string conversion (advanced)
In Chrome DevTools:
- Set breakpoints where string building occurs (e.g., functions that format messages)
- Inspect variables in the scope
- If it’s a React app, use React DevTools to inspect props/state and see which value is an object.
For Node.js debugging:
- Use
node --inspectand Chrome DevTools - Or use VS Code’s “Attach to Node Process” and set breakpoints in formatting utilities.
3.5 Confirm the real runtime type
Sometimes the value isn’t a plain object. It could be a class instance, Error, Date, Map, etc.
Use:
jsconsole.log(typeof value); console.log(value?.constructor?.name); console.log(Object.prototype.toString.call(value));
Examples:
Object.prototype.toString.call(new Date())→"[object Date]"Object.prototype.toString.call([])→"[object Array]"
If you see [object Object], it’s usually a plain object or an instance inheriting the default toString.
4) Serialization: JSON.stringify isn’t always enough
Many fixes recommend JSON.stringify(obj), and it’s often correct—but there are caveats.
4.1 Circular references
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
Fix options:
-
Use a safe stringifier like
fast-safe-stringify. -
Implement a circular replacer:
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; }); }
4.2 BigInt
jsJSON.stringify({ n: 1n }); // TypeError: Do not know how to serialize a BigInt
Fix with a replacer:
jsJSON.stringify({ n: 1n }, (k, v) => (typeof v === "bigint" ? v.toString() : v));
4.3 Lossy serialization (Dates, Maps, Sets, Errors)
Datebecomes an ISO string (often OK)Map/Setbecome{}by defaultErrorlosesmessage/stackunless handled
Example:
jsJSON.stringify(new Error("boom")); // "{}"
A better error serializer:
jsfunction serializeError(err) { return { name: err.name, message: err.message, stack: err.stack, cause: err.cause }; }
If you want robust handling for many types, consider libraries like:
superjson(common in Next.js/tRPC ecosystems)serialize-error(focused on Error objects)devalue(safe-ish serialization for embedding in HTML)
5) UI best practices: don’t render raw objects
5.1 React example: rendering API responses
Bad:
jsxfunction DebugPanel({ data }) { return <div>{data}</div>; // will error or render poorly }
Better:
jsxfunction DebugPanel({ data }) { return <pre style={{ whiteSpace: "pre-wrap" }}> {JSON.stringify(data, null, 2)} </pre>; }
5.2 User-facing text: choose stable formatting
If this is user-visible (toast messages, error banners), raw JSON is rarely appropriate.
Instead:
- Extract key fields (
message,code,id) - Provide a human-friendly fallback
Example:
tstype ApiError = { code?: string; message?: string; details?: unknown }; function formatError(e: unknown): string { if (typeof e === "string") return e; if (e && typeof e === "object") { const any = e as ApiError; if (any.message) return any.message; if (any.code) return `Error code: ${any.code}`; } return "An unexpected error occurred."; }
6) Logging and observability: stop concatenating strings
The [object Object] problem is often a logging anti-pattern.
6.1 Prefer structured logging
Instead of:
jslogger.info("request=" + req);
Use:
jslogger.info({ reqId: req.id, path: req.path, query: req.query }, "incoming request");
This works best with structured loggers:
- Pino (fast, JSON logs, great for Node)
- Winston (flexible transports, heavier)
- Bunyan (older but structured)
Quick comparison (practical view)
- Pino: best default choice for high-throughput services; pairs well with
pino-prettylocally. - Winston: useful when you need many transports and legacy integrations; easier to misconfigure into unstructured logs.
6.2 Browser logging: use objects as separate args
jsconsole.log("state update", { prev, next, action });
Chrome will let you expand objects; no stringification required.
6.3 Traces and correlation IDs
Sometimes [object Object] shows up only in production logs. When you’re chasing it:
- Ensure logs include a request ID / trace ID.
- Link the UI error to the API request that produced it.
In Node, you can propagate correlation IDs using AsyncLocalStorage.
7) TypeScript: prevent these bugs at compile time
A big reason [object Object] appears is that something typed as any (or unknown mishandled) ends up in a string context.
7.1 Prefer unknown over any
If a function accepts arbitrary input, use unknown and narrow:
tsfunction toUserMessage(value: unknown): string { if (typeof value === "string") return value; if (value && typeof value === "object" && "message" in value) { const msg = (value as any).message; if (typeof msg === "string") return msg; } return "Unexpected value"; }
7.2 Use ESLint rules to catch risky coercion
Useful rules:
@typescript-eslint/restrict-plus-operands(prevents"" + objpatterns)@typescript-eslint/no-base-to-string(flags object-to-string)no-implicit-coercion
Example ESLint config snippet:
json{ "rules": { "@typescript-eslint/no-base-to-string": "error", "@typescript-eslint/restrict-plus-operands": "error", "no-implicit-coercion": "error" } }
These rules dramatically reduce accidental [object Object] regressions.
8) If you do want a string: define a meaningful conversion
Sometimes you actually want objects to have a human-readable string form.
8.1 Custom toString()
jsclass User { constructor(id, name) { this.id = id; this.name = name; } toString() { return `User(${this.id}, ${this.name})`; } } const u = new User(1, "Ava"); "User: " + u; // "User: User(1, Ava)"
8.2 Symbol.toPrimitive for fine-grained 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 m = new Money(1234); String(m); // "$12.34" +m; // 12.34
Use this sparingly—it’s powerful but can make debugging harder if overused.
9) Backend/API boundaries: avoid stringly-typed interfaces
A frequent real-world cause:
- API returns JSON
- Client expects a string and displays it
- But API returns an object (e.g., error payload)
9.1 Validate API responses
Use runtime validation libraries:
zodvalibotio-ts
Example with Zod:
tsimport { z } from "zod"; const UserSchema = z.object({ id: z.number(), name: z.string() }); type User = z.infer<typeof UserSchema>; async function fetchUser(id: number): Promise<User> { const res = await fetch(`/api/users/${id}`); const json = await res.json(); return UserSchema.parse(json); }
If the server returns { error: ... } unexpectedly, parsing fails early with a clear error—better than rendering [object Object].
9.2 Normalize error shapes
Define and enforce an error contract, e.g.:
tstype ErrorResponse = { error: { code: string; message: string; requestId?: string; details?: unknown; }; };
Then, in the UI, render error.message rather than the whole object.
10) Practical debugging case studies
Case study A: [object Object] in a toast notification
Symptom: UI toast shows “Failed: [object Object]”.
Typical cause:
jstoast.error("Failed: " + err);
err is an object from an HTTP client (Axios error) or a custom error payload.
Fix:
jstoast.error(`Failed: ${formatError(err)}`);
Where formatError safely extracts message text.
Debug tip:
jsconsole.log("err=", err); console.log("err keys:", err && typeof err === "object" ? Object.keys(err) : null);
For Axios:
err.messageerr.response?.dataerr.response?.status
Case study B: [object Object] in query string
Symptom: URL becomes ?filters=[object%20Object].
Cause: URLSearchParams with nested object.
Fix:
filters: JSON.stringify(filters)- Or use
qs.stringify(filters, { addQueryPrefix: true })for nested params.
Case study C: [object Object] in server logs
Symptom: logs show payload=[object Object].
Cause: string concatenation.
Fix: structured logs:
jslogger.info({ payload }, "incoming payload");
Bonus: With Pino + JSON, you can query payload.userId in log tools.
11) Best practices checklist
For application code
- Avoid
"" + objand`${obj}`unless you are sureobjis a string. - Render objects intentionally: pick fields or pretty-print in debug-only UI.
- Use
formatError(unknown)to convert unknown errors to user-friendly messages. - Validate API responses at runtime when correctness matters.
For TypeScript and linting
- Prefer
unknownfor untrusted data. - Enable ESLint rules:
@typescript-eslint/no-base-to-string@typescript-eslint/restrict-plus-operandsno-implicit-coercion
For logging/observability
- Use structured logging with Pino/Winston rather than concatenated strings.
- Log objects as objects (
logger.info({ obj })). - Add correlation IDs and include them in errors.
For serialization
- Use
JSON.stringifywith a replacer for BigInt. - Use safe stringification when circular refs are possible.
- Serialize Errors explicitly (message, stack, cause).
12) A reusable utility: safe display + safe logging
Here’s a practical utility you can drop into many codebases:
tsexport function safeToString(value: unknown): string { if (value == null) return String(value); // "null" or "undefined" if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); if (typeof value === "bigint") return value.toString(); if (value instanceof Error) { return value.message || value.name; } // Try JSON first, but handle circulars try { const seen = new WeakSet<object>(); return JSON.stringify(value, (k, v) => { if (typeof v === "bigint") return v.toString(); if (typeof v === "object" && v !== null) { if (seen.has(v)) return "[Circular]"; seen.add(v); } if (v instanceof Error) { return { name: v.name, message: v.message, stack: v.stack }; } return v; }); } catch { // Final fallback return Object.prototype.toString.call(value); } }
Usage:
- UI toast:
toast.error(safeToString(err)) - Defensive logging:
logger.warn({ err }, "operation failed")plus user message derived fromsafeToString(err)
Note: even with safeToString, prefer structured logging for logs. The string form is mainly for user-facing text or environments where structured objects can’t be displayed.
Closing thoughts
"[object Object]" isn’t just an annoying string—it’s a signal that your code is crossing a boundary (UI text, URL, log line, or external interface) without an intentional representation of data.
Fixing it well usually means:
- Using structured logging rather than concatenation
- Being explicit about serialization
- Enforcing contracts with TypeScript + runtime validation
- Treating unknown values as
unknownand narrowing safely
Once these practices are in place, [object Object] transitions from a recurring mystery to a rare, quickly diagnosed edge case.
![Diagnosing and Preventing “[object Object]” Bugs in JavaScript: Serialization, Logging, and Data Handling for Engineers](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_e0c3363d6f.png)