Why you’re seeing “[object Object]” (and why it’s so common)
If you’ve built web applications in JavaScript (browser or Node.js), you’ve almost certainly seen the string:
[object Object]
It tends to show up:
- In UI output where you expected meaningful text
- In logs where you expected a structured object
- In error messages or toast notifications
- In HTTP responses (especially from older codepaths or poorly handled exceptions)
The key point: “[object Object]” is not an error by itself. It’s usually a symptom that an object was coerced into a string implicitly (or stringified incorrectly), often losing critical information in the process.
At a technical level, this string comes from calling Object.prototype.toString() (directly or indirectly) on a plain object:
jsString({ a: 1 }) // "[object Object]" ({ a: 1 }).toString() // "[object Object]" "" + { a: 1 } // "[object Object]"
Understanding where coercion happens, and how to intentionally render objects, is the path to eliminating these bugs.
The JavaScript coercion rule that causes it
JavaScript performs implicit type conversions in many contexts:
- String concatenation:
"value: " + someValue - Template literals:
`value: ${someValue}` - DOM APIs that expect strings (e.g.,
element.textContent = obj) - Logging frameworks that do naive string interpolation
- Error creation:
new Error(obj)
When JavaScript needs a string, it tries (simplified) the following:
- If the value is already a string, use it.
- If it’s a primitive (number, boolean, bigint, symbol), convert accordingly.
- If it’s an object, call
ToPrimitive:- Try
obj[Symbol.toPrimitive]if present - Else call
obj.toString()thenobj.valueOf()(or vice versa depending on hint)
- Try
For plain objects, .toString() is inherited from Object.prototype.toString and returns "[object Object]".
Common implicit coercion traps
1) String concatenation
jsconst payload = { userId: 123, role: "admin" }; console.log("payload=" + payload); // payload=[object Object]
2) Template literals (often misunderstood; they still stringify)
jsconsole.log(`payload=${payload}`); // payload=[object Object]
3) DOM rendering
jsconst el = document.querySelector("#output"); el.textContent = { a: 1 }; // shows: [object Object]
4) Throwing or wrapping errors incorrectly
jsthrow new Error({ message: "failed" }); // Error: [object Object]
Real-world scenarios where “[object Object]” appears
Scenario A: UI notification shows “[object Object]”
Example in a React app:
jsxfunction save() { api.saveForm(form) .catch(err => { toast.error("Save failed: " + err); }); }
If err is an object (Axios error, custom error, or server JSON), concatenation produces [object Object].
Better: extract a message (or serialize)
jsxtoast.error(`Save failed: ${getErrorMessage(err)}`); function getErrorMessage(err) { if (!err) return "Unknown error"; if (typeof err === "string") return err; if (err instanceof Error) return err.message; // axios-like if (err.response?.data) { const data = err.response.data; if (typeof data === "string") return data; if (data.message) return data.message; return safeJson(data); } if (err.message) return err.message; return safeJson(err); } function safeJson(value) { try { return JSON.stringify(value); } catch { return String(value); } }
This keeps UI messages human-readable and prevents lossy coercion.
Scenario B: Server logs are useless
In Node.js:
jslogger.info("request body=" + req.body);
If req.body is an object, the log becomes [object Object]. Worse: you lose the ability to search logs for fields.
Fix: structured logging
jslogger.info({ body: req.body }, "request body");
This is supported by loggers like pino and bunyan and is the recommended approach.
If you must interpolate a string:
jslogger.info("request body=%s", JSON.stringify(req.body));
(Though %o / %O formatting may work better depending on your logger; see tool comparisons below.)
Scenario C: API responds with “[object Object]”
Express example:
jsapp.use((err, req, res, next) => { res.status(500).send("Internal error: " + err); });
If err is an object, user sees [object Object].
Better error middleware:
jsapp.use((err, req, res, next) => { const status = err.statusCode || err.status || 500; const message = err.expose ? err.message : "Internal Server Error"; // Log full details server-side req.log?.error({ err }, "Unhandled error"); res.status(status).json({ error: message, requestId: req.id, }); });
This avoids leaking internals while remaining debuggable.
Correct ways to turn objects into readable strings
1) JSON.stringify (with caveats)
jsconsole.log(JSON.stringify({ a: 1, b: 2 })); // {"a":1,"b":2}
Pretty print:
jsconsole.log(JSON.stringify(obj, null, 2));
Caveats:
- Fails on circular references
- Drops
undefined, functions, and symbols - Serializes
Errorpoorly by default
To handle circular references, consider:
safe-stable-stringifyfast-safe-stringify
Example:
jsimport stringify from "fast-safe-stringify"; logger.info("payload=%s", stringify(payload));
2) console.log(obj) (don’t force it into a string)
Instead of:
jsconsole.log("obj=" + obj);
Do:
jsconsole.log("obj=", obj);
In Chrome DevTools and Node, this gives expandable objects and better inspection.
3) Node’s util.inspect
For logs where JSON is too strict:
jsimport util from "node:util"; console.log(util.inspect(obj, { depth: 5, colors: true }));
This prints more naturally than JSON, including types like Map, Set, and functions.
4) Provide a domain-specific string format
For key objects that appear in logs often, implement a meaningful representation.
jsclass User { constructor(id, email) { this.id = id; this.email = email; } toString() { return `User(id=${this.id}, email=${this.email})`; } } String(new User(1, "a@b.com")); // User(id=1, email=a@b.com)
This is great for developer ergonomics, but note:
toString()should be fast and not throw- Don’t include secrets (tokens, passwords)
5) Customize coercion via Symbol.toPrimitive
Advanced (use sparingly):
jsclass Money { constructor(amount, currency) { this.amount = amount; this.currency = currency; } [Symbol.toPrimitive](hint) { if (hint === "string") return `${this.amount} ${this.currency}`; return this.amount; } } const m = new Money(10, "USD"); `${m}`; // "10 USD" +m; // 10
This is powerful but can surprise teammates if overused.
Debugging techniques: finding where “[object Object]” originates
When a UI shows [object Object], the fix is usually not “replace with JSON.stringify everywhere.” You want to identify:
- What exact object is being coerced?
- Where does that conversion occur?
- Is the object itself wrong (unexpected type), or is rendering wrong?
1) Use stack traces by forcing an exception at coercion points
If you suspect coercion, you can temporarily detect it.
For example, wrap a suspect value:
jsfunction assertString(value, label) { if (typeof value !== "string") { const err = new Error(`${label} is not a string`); err.details = { valueType: typeof value, value }; throw err; } return value; } // usage el.textContent = assertString(message, "message");
This converts a silent coercion bug into an actionable stack trace.
2) Add “type logging” before rendering
jsconsole.log("message type:", typeof message, "value:", message);
If it’s object, inspect it and decide how to extract the right string.
3) Use DevTools breakpoints on DOM mutations (frontend)
In Chrome DevTools:
- Inspect the element that contains
[object Object] - Right-click → Break on → subtree modifications / attribute modifications
When it breaks, you’ll see the call stack leading to textContent/innerHTML assignment.
4) Use network inspection to see if the server sent an object
Sometimes the backend sends JSON like:
json{ "error": { "code": "INVALID", "message": "Bad input" } }
…and the frontend does:
jsshowError(data.error);
…where showError expects a string.
Check the Network tab payload and response body to confirm the exact shapes.
5) Add runtime schema validation (mid/large codebases)
Tools like zod, io-ts, valibot, or TypeBox help ensure your “message” is actually a string.
Example with zod:
tsimport { z } from "zod"; const ErrorResponse = z.object({ error: z.string(), }); const parsed = ErrorResponse.safeParse(await res.json()); if (!parsed.success) { console.error(parsed.error.format()); throw new Error("Unexpected error response shape"); } showError(parsed.data.error);
This prevents [object Object] from creeping into UI paths due to shape drift.
Tool and library behavior: why some logs show objects and others show “[object Object]”
Console APIs
console.log("x=", obj)prints the object structure.console.log("x=" + obj)forces string coercion.console.log("%o", obj)(Chrome/Node) prints as an object.
Node.js util.format specifiers
jsimport util from "node:util"; util.format("%s", { a: 1 }); // "[object Object]" util.format("%o", { a: 1 }); // "{ a: 1 }" (inspect-style) util.format("%j", { a: 1 }); // '{"a":1}' but fails on circular
This matters because many logging libraries use util.format under the hood.
Logging libraries
- pino: best with structured objects (
logger.info({obj})), extremely fast JSON logs. - winston: flexible transports, but can easily end up stringifying incorrectly if you build messages with concatenation.
- debug package: supports
%o/%Oformats.
Best practice: choose a logger that supports structured logging, and pass objects as objects—not interpolated strings.
Handling Error objects properly (a frequent culprit)
A surprisingly common “[object Object]” bug comes from catching something that isn’t an Error.
Problem: catch (e) can be anything
In JavaScript, throw is not restricted:
jsthrow { message: "nope", code: "X" };
Then:
jscatch (e) { console.error("failed: " + e); // [object Object] }
Best practice: normalize thrown values
jsfunction normalizeError(e) { 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)); } } try { // ... } catch (e) { const err = normalizeError(e); console.error(err); }
Better: throw only Error (or subclasses)
In shared code guidelines, enforce:
- Always
throw new Error("message") - For app errors, create a typed error class:
tsclass AppError extends Error { constructor(message: string, public code: string, public status = 400) { super(message); this.name = "AppError"; } } throw new AppError("Invalid email", "INVALID_EMAIL", 422);
Then format for users and logs separately.
Frontend best practices to avoid “[object Object]”
1) Don’t render raw objects
In React:
jsxreturn <div>{someObject}</div>;
React will actually throw for plain objects in many cases (“Objects are not valid as a React child”). But you can still end up with [object Object] in attributes or when coercing manually.
Instead:
- Render a specific field:
{someObject.message} - Or format as JSON for debugging only:
jsx<pre>{JSON.stringify(someObject, null, 2)}</pre>
2) Centralize error display logic
Create a single error-to-string function used across the app (getErrorMessage as shown earlier). This prevents inconsistent formatting across components.
3) Use TypeScript types to prevent misuse
Example:
tstype ToastInput = { message: string }; function showToast(input: ToastInput) { toast(input.message); } // compile error if you pass an object without message showToast({ message: "Saved" });
For API responses, define explicit DTOs and validate if needed.
Backend best practices to avoid “[object Object]”
1) Use structured logging, not concatenated strings
Bad:
jslogger.error("err=" + err);
Good:
jslogger.error({ err }, "request failed");
2) Standardize error response shape
Return:
json{ "error": "Human-readable message", "code": "SOME_CODE" }
Not:
json{ "error": { "message": "...", "details": { ... } } }
If you need details, include them behind a debug flag or in logs.
3) Sanitize logs and outputs
Never stringify entire request objects that may contain secrets:
- Authorization headers
- Cookies
- Passwords
- Tokens
Use redaction features in loggers (pino has built-in redaction).
Practical recipes (copy/paste)
Recipe 1: Safe object printing for debugging
jsexport function debugValue(value) { if (typeof value === "string") return value; if (value instanceof Error) { return value.stack || value.message; } try { return JSON.stringify(value, null, 2); } catch { return String(value); } }
Recipe 2: Express error middleware with proper logging
jsapp.use((err, req, res, next) => { const status = err.statusCode ?? err.status ?? 500; // Log as an object, not as a string req.log?.error({ err, status, path: req.path }, "Unhandled error"); const clientMessage = status >= 500 ? "Internal Server Error" : (err.message || "Request failed"); res.status(status).json({ error: clientMessage, }); });
Recipe 3: Avoid coercion in logs
js// Bad logger.info("user=" + user); // Good logger.info({ user }, "user"); // If you need a single message field logger.info("user=%o", user);
A mental model to prevent these bugs long-term
When you see [object Object], ask:
- Where did string coercion happen? (concatenation, template literal, DOM assignment, error construction)
- What type did you expect? (string, number, structured object)
- What type did you actually have? (inspect the runtime value)
- What should the output be?
- Human-readable message? Extract
.message. - Machine-readable? Use JSON.
- Debug-only? Use
util.inspector devtools object logging.
- Human-readable message? Extract
- Can the type mismatch be prevented earlier?
- TypeScript types
- Runtime validation
- Standardized API shapes
- Consistent error classes
This approach keeps fixes from becoming ad-hoc JSON.stringify patches scattered throughout the codebase.
Common anti-patterns (and what to do instead)
Anti-pattern: "Error: " + err
Instead:
jslogger.error({ err }, "Error occurred"); // and/or toast.error(getErrorMessage(err));
Anti-pattern: Returning raw error objects from APIs
Instead: return a stable public shape and log details server-side.
Anti-pattern: JSON.stringify everything
This can leak secrets, break on circular refs, and produce noisy logs.
Instead: choose:
- structured logging for logs
- explicit formatting for UI
- safe stringifiers only where necessary
Summary checklist
- Avoid string concatenation with objects: use
console.log("x=", obj)or structured logging. - Use
JSON.stringifyintentionally (and safely) when you need JSON. - Normalize errors: ensure you throw
Errorobjects and extract messages consistently. - Add schema validation or TypeScript types where boundaries exist (API responses, event payloads).
- Debug by locating the coercion site (DOM breakpoints, stack traces, runtime type logging).
Eliminating “[object Object]” is less about that specific string and more about building reliable, explicit formatting and error-handling pathways across your application.
![Diagnosing and Fixing “[object Object]” Bugs in JavaScript: Root Causes, Debugging Techniques, and Best Practices](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_ef3a258e87.png)