Why you’re seeing [object Object]
If you’ve ever logged something and gotten the unhelpful string:
[object Object]
…you’ve hit one of the most common “I know it’s an object, but which object?” problems in JavaScript.
[object Object] is not an error by itself. It’s the default string representation of a plain JavaScript object when it gets coerced to a string. This can happen in many places: string concatenation, template interpolation (in some cases), DOM rendering, logging systems, HTTP query building, or UI frameworks.
The goal of this article is to show:
- What
[object Object]actually means (and where it comes from) - How to correctly inspect objects while debugging
- How to serialize objects safely (including edge cases like circular references and BigInt)
- How to prevent it in UIs, logs, and APIs
- Tooling tips for browser DevTools, Node.js, and observability systems
This is written for software engineers: approachable for juniors, but with enough detail for seniors who care about correctness, performance, and production diagnostics.
The root cause: JavaScript’s string coercion
When JavaScript needs a string and you give it an object, it performs type coercion. The high-level flow is:
- Try to convert the object to a primitive via
obj[Symbol.toPrimitive], thenvalueOf(), thentoString(). - If it ends up calling
toString()on a plain object, it returns"[object Object]".
You can see it directly:
jsconst obj = { a: 1 }; String(obj); // "[object Object]" obj + ""; // "[object Object]" "Value: " + obj; // "Value: [object Object]"
Object.prototype.toString is the default implementation and returns a string like:
jsObject.prototype.toString.call({}); // "[object Object]"
Notably, different built-ins have different tags:
jsObject.prototype.toString.call([]); // "[object Array]" Object.prototype.toString.call(new Date()); // "[object Date]" Object.prototype.toString.call(/re/); // "[object RegExp]"
So if you’re seeing [object Object], it usually means: a plain object was coerced into a string.
Common places [object Object] appears
1) String concatenation and template literals
Concatenation is the classic culprit:
jsconsole.log("User: " + user); // User: [object Object]
Template literals can also do it:
jsconsole.log(`User: ${user}`); // User: [object Object]
The fix is to access a meaningful property:
jsconsole.log(`User: ${user.name} (${user.id})`);
Or serialize:
jsconsole.log(`User: ${JSON.stringify(user)}`);
2) DOM rendering and innerHTML/text
If you do:
jsconst el = document.getElementById("output"); el.textContent = someObject;
You’ll get [object Object] in the DOM. Use:
jsel.textContent = JSON.stringify(someObject, null, 2);
Or render specific fields.
3) Query strings and URLSearchParams
This is subtle. You might build a URL like:
jsconst params = new URLSearchParams({ filter: { status: "open" } }); params.toString(); // "filter=%5Bobject+Object%5D"
This happens because URLSearchParams expects string-ish values.
A better approach:
jsconst filter = { status: "open" }; const params = new URLSearchParams({ filter: JSON.stringify(filter), });
Or flatten the shape:
jsconst params = new URLSearchParams({ status: "open", });
4) Logging systems that stringify arguments
Some loggers coerce objects poorly:
jslogger.info("payload=" + payload); // payload=[object Object]
Prefer structured logging:
jslogger.info({ payload }, "received payload");
Depending on your logger (Pino, Winston, Bunyan), object handling differs. More on this later.
5) Error messages and thrown objects
Throwing plain objects is legal but a bad practice:
jsthrow { code: 400, message: "Bad Request" };
Many runtimes/loggers will show something like [object Object] or lose stack traces.
Instead, throw Error (or subclasses) and attach metadata:
jsclass HttpError extends Error { constructor(status, message, details) { super(message); this.name = "HttpError"; this.status = status; this.details = details; } } throw new HttpError(400, "Bad Request", { field: "email" });
The debugging mindset: don’t stringify blindly
When you see [object Object], the temptation is to “just stringify it.” That can work, but as your systems grow you’ll run into pitfalls:
- Circular references crash
JSON.stringify BigIntcrashesJSON.stringifyErrorobjects stringify poorly (you lose stack/message)- Large payloads become noisy and expensive
- Privacy/security risks (PII in logs)
Instead, start with inspection tools and then choose a representation deliberately.
Better ways to inspect objects (browser and Node)
1) Use console.log(obj) (not string concatenation)
In Chrome/Firefox DevTools, this is often enough:
jsconsole.log(user);
DevTools provides expandable views, live object references, and search.
Tip: If you mutate the object after logging, some consoles display the current state when you expand it later. If you need a snapshot, serialize (carefully) or deep-clone.
2) Use console.dir(obj, { depth: ... }) (Node-friendly)
In Node.js:
jsconsole.dir(user, { depth: null, colors: true });
This gives deep expansion and is more predictable than console.log in some cases.
3) Use Node’s util.inspect
jsimport util from "node:util"; console.log(util.inspect(user, { depth: null, colors: true }));
This is useful if you need a string output for logs but want richer formatting than JSON.
4) Log as structured JSON (when appropriate)
If you truly want JSON (e.g., for ingestion by log platforms):
jsconsole.log(JSON.stringify({ event: "user_loaded", userId: user.id }));
Keep it small and purposeful.
The “right” way to stringify: JSON.stringify (and its limits)
Basic usage
jsJSON.stringify({ a: 1 }); // '{"a":1}'
Pretty printing:
jsJSON.stringify({ a: 1, b: { c: 2 } }, null, 2);
Common pitfalls
1) Circular references
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
2) BigInt
jsJSON.stringify({ id: 10n }); // TypeError: Do not know how to serialize a BigInt
3) undefined, functions, symbols
jsJSON.stringify({ x: undefined, f: () => {} }); // "{}" JSON.stringify([undefined]); // "[null]"
4) Errors serialize poorly
jsJSON.stringify(new Error("boom")); // "{}"
Errors have non-enumerable properties by default.
Safe serialization patterns
1) Use a safe stringify library
Two popular choices:
- fast-safe-stringify: fast, handles circular references
- safe-stable-stringify: stable key ordering + circular handling
Example:
jsimport safeStringify from "fast-safe-stringify"; const str = safeStringify(payload);
This avoids crashes in logging code paths.
2) Custom replacer to handle BigInt and Error
If you want to stay dependency-free:
jsfunction jsonReplacer(key, value) { if (typeof value === "bigint") return value.toString(); if (value instanceof Error) { return { name: value.name, message: value.message, stack: value.stack, ...value, // include enumerable custom fields }; } return value; } const s = JSON.stringify(obj, jsonReplacer, 2);
Note: this still fails on circular references.
3) Circular reference handling with WeakSet
jsfunction safeReplacer() { const seen = new WeakSet(); return (key, value) => { if (typeof value === "bigint") return value.toString(); if (value instanceof Error) { return { name: value.name, message: value.message, stack: value.stack }; } if (typeof value === "object" && value !== null) { if (seen.has(value)) return "[Circular]"; seen.add(value); } return value; }; } const s = JSON.stringify(obj, safeReplacer(), 2);
This is a good general-purpose fallback for debug logs.
Preventing [object Object] in UI code
React
Rendering an object directly:
jsxreturn <div>{user}</div>; // renders [object Object]
Better:
jsxreturn <div>{user.name}</div>;
Or for debug-only:
jsxreturn <pre>{JSON.stringify(user, null, 2)}</pre>;
Senior tip: do not ship huge JSON blobs into the DOM in production—performance and security risks.
Vue
html<div>{{ user }}</div> <!-- [object Object] -->
Instead:
html<div>{{ user.name }}</div> <pre>{{ JSON.stringify(user, null, 2) }}</pre>
Or define a computed debug string.
Angular
Angular will also call toString for interpolations. Use the json pipe:
html<pre>{{ user | json }}</pre>
Preventing [object Object] in HTTP and APIs
Fetch: request bodies must be strings (typically JSON)
Bad:
jsfetch("/api", { method: "POST", body: { a: 1 }, });
This won’t automatically stringify and may fail or produce unexpected results.
Good:
jsfetch("/api", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ a: 1 }), });
Express: don’t concatenate objects in logs
Bad:
jsapp.post("/api", (req, res) => { console.log("body=" + req.body); // [object Object] res.sendStatus(200); });
Good:
jsapp.post("/api", (req, res) => { console.dir(req.body, { depth: null }); res.sendStatus(200); });
Production: use structured logging, redact sensitive fields.
Overriding object stringification (advanced)
Sometimes you want meaningful string output when an object is coerced.
Option 1: Define toString()
jsclass User { constructor(id, name) { this.id = id; this.name = name; } toString() { return `User(${this.id}, ${this.name})`; } } const u = new User(1, "Asha"); "User is: " + u; // "User is: User(1, Asha)"
Option 2: Define Symbol.toPrimitive
More precise control for different coercions:
jsclass Money { constructor(cents) { this.cents = cents; } [Symbol.toPrimitive](hint) { if (hint === "number") return this.cents; return `$${(this.cents / 100).toFixed(2)}`; } } const m = new Money(1234); String(m); // "$12.34" Number(m); // 1234
Caution: Overriding these can be surprising for other engineers. Use it for domain objects where string coercion is truly meaningful (IDs, money, URIs), and document it.
Logging best practices: avoid [object Object] and improve observability
Prefer structured logging over string concatenation
Instead of:
jslogger.info("request=" + req);
Do:
jslogger.info({ method: req.method, path: req.path, requestId: req.id, }, "incoming request");
This makes logs queryable in ELK/Datadog/Splunk and avoids coercion issues.
Tool comparison: Pino vs Winston (practical notes)
- Pino: very fast, JSON-first, great for production structured logs. Requires pretty printing in dev (e.g.,
pino-pretty). - Winston: flexible transports and formatting; can be slower and more configuration-heavy.
If your stack is Node.js microservices and you care about throughput, Pino is a common default.
Example Pino usage:
jsimport pino from "pino"; const logger = pino({ level: process.env.LOG_LEVEL ?? "info" }); logger.info({ userId: 123, plan: "pro" }, "user upgraded");
Redaction and PII controls
Blindly stringifying an object can leak tokens, passwords, emails.
Strategies:
- Redact known keys (e.g.,
password,authorization,token) - Whitelist fields instead of blacklisting when possible
- Log IDs, not full records
Pino supports redaction:
jsconst logger = pino({ redact: { paths: ["req.headers.authorization", "user.email"], remove: true, }, });
Debugging techniques when [object Object] shows up in production
1) Search for implicit string coercion
Common patterns to grep for:
"" + something"text" + obj`${obj}`innerHTML = obj/textContent = objnew URLSearchParams({ key: obj })
In TypeScript, enabling noImplicitAny and using stricter types often helps surface mistakes earlier.
2) Add type-aware assertions
In critical paths:
jsfunction assertString(value, name) { if (typeof value !== "string") { throw new TypeError(`${name} must be a string, got ${typeof value}`); } }
This prevents accidental object-to-string coercion from silently making it to UI/logs.
3) Use runtime inspection
When you only know you got [object Object], try to locate the origin by logging the value before coercion:
jsconsole.log({ value, type: typeof value, ctor: value?.constructor?.name });
More robust:
jsfunction describe(value) { return { type: typeof value, isArray: Array.isArray(value), tag: Object.prototype.toString.call(value), ctor: value && value.constructor ? value.constructor.name : null, keys: value && typeof value === "object" ? Object.keys(value).slice(0, 20) : null, }; } console.log(describe(value));
4) Source maps and stack traces
If [object Object] appears in an error string in the UI, you may be concatenating an object into an error message. Prefer:
jscatch (err) { console.error("Request failed", err); }
Over:
jscatch (err) { console.error("Request failed: " + err); }
In production builds, ensure source maps are correctly uploaded (Sentry, Datadog, etc.) so you can find the actual location.
TypeScript strategies to prevent [object Object]
TypeScript won’t automatically prevent coercion, but good typing reduces surprises.
1) Avoid any and unknown without narrowing
If something is any, it can be accidentally used as a string.
Prefer unknown and narrow:
tsfunction renderValue(value: unknown): string { if (typeof value === "string") return value; if (typeof value === "number") return String(value); return JSON.stringify(value); }
2) Use template-safe helpers
For UI:
tsexport function formatUserLabel(user: { id: string; name: string }) { return `${user.name} (${user.id})`; }
Centralizing formatting prevents random components from doing "" + user.
3) Lint rules
ESLint can catch suspicious patterns:
@typescript-eslint/restrict-template-expressionshelps ensure template expressions are strings (or explicitly allowed types).no-useless-concatand code review guidelines help reduce accidental concatenation.
Performance considerations
Stringifying large objects is expensive and can:
- Increase latency in hot paths
- Inflate log volume and costs
- Create GC pressure
Recommendations:
- Log small: IDs, sizes, counts, key fields
- Use sampling for verbose logs
- Ensure heavy serialization is gated behind log level:
jsif (logger.isLevelEnabled?.("debug")) { logger.debug({ payload }, "payload details"); }
If your logger doesn’t have level guards, consider adding them or using lazy evaluation patterns.
A practical “fix checklist”
When you encounter [object Object], follow this sequence:
- Find the coercion point: look for concatenation/interpolation/DOM assignment/log formatting.
- Inspect the raw value with
console.log(value)orconsole.dir. - Decide the intent:
- If it’s UI: render specific fields or use a
json/JSON.stringifydebug view. - If it’s logs: use structured logging and redact sensitive data.
- If it’s a URL/query: flatten or
JSON.stringifyfor transport.
- If it’s UI: render specific fields or use a
- If serialization is needed, choose:
JSON.stringifyfor simple safe JSON- safe stringify (library or WeakSet) for circular/complex objects
- Add guardrails: types, lint rules, and helper formatters.
Examples: before/after fixes
Example A: Broken log message
Before:
jsfunction handleCheckout(cart) { logger.info("cart=" + cart); }
After (structured):
jsfunction handleCheckout(cart) { logger.info({ itemCount: cart.items?.length, total: cart.total, cartId: cart.id, }, "checkout started"); }
Example B: Broken UI rendering
Before (React):
jsxexport function Profile({ user }) { return <div className="name">{user}</div>; }
After:
jsxexport function Profile({ user }) { return <div className="name">{user.name}</div>; }
Example C: Query param bug
Before:
jsconst url = "/issues?" + new URLSearchParams({ filter: { status: "open" } });
After:
jsconst filter = { status: "open" }; const url = "/issues?" + new URLSearchParams({ filter: JSON.stringify(filter) });
Closing thoughts
[object Object] is a symptom of ambiguous intent: the code asked for a string but was handed a structured value. The most reliable fix is not to “make it go away” with random stringification, but to be explicit about:
- what you’re trying to display
- what you’re trying to transmit
- what you’re trying to log
With a few habits—structured logging, safe serialization, and strict typing/linting—you can eliminate [object Object] from your UI and logs and replace it with output that’s genuinely useful for debugging and operations.
![Debugging and Fixing “[object Object]”: A Practical Guide for JavaScript Developers](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_885d22c271.png)