The problem with “[object Object]”
If you’ve ever logged something in JavaScript and seen the output "[object Object]", you’ve encountered one of the most common—and most misleading—debugging experiences in the language.
It’s not an error. It’s not a special object type. It’s simply the result of coercing an object into a string using JavaScript’s default rules.
The frustrating part is that it often appears exactly when you need the most visibility: while debugging request payloads, errors, state objects, event data, or API responses. Instead of a helpful view, you get a useless placeholder.
This article breaks down:
- Why
"[object Object]"happens (the real mechanics) - How it shows up in common contexts (console logs, DOM, frameworks, Node)
- The best techniques to inspect objects reliably
- How to avoid it in production logging and error reporting
- Best practices for serialization, structured logs, and safe redaction
The goal is to make sure you never lose time to this output again—whether you’re a junior debugging in Chrome DevTools or a senior engineer building observability across microservices.
1. Where does "[object Object]" come from?
At the core, this output comes from JavaScript’s Object.prototype.toString() or from default string coercion.
1.1 String coercion in JavaScript
When JavaScript needs to convert a value to a string, it uses a set of rules. For objects, those rules typically call:
obj[Symbol.toPrimitive]if present- Otherwise
obj.toString()and/orobj.valueOf()depending on the hint
For plain objects ({}), toString() is inherited from Object.prototype.
jsconst o = { a: 1 }; String(o); // "[object Object]" '' + o; // "[object Object]" `${o}`; // "[object Object]" o.toString(); // "[object Object]"
So when you see "[object Object]", it usually means something tried to treat an object like a string.
1.2 Why that exact string?
Object.prototype.toString() returns a string in this format:
[object Type]
For a plain object, the “type tag” is Object, hence:
[object Object]
You can see different tags:
jsObject.prototype.toString.call([]); // "[object Array]" Object.prototype.toString.call(new Date()); // "[object Date]" Object.prototype.toString.call(/re/); // "[object RegExp]"
This is sometimes useful for type checking, but it is not a good representation of object contents.
2. The most common places you’ll see it
2.1 Concatenating objects into strings
jsconst user = { id: 1, name: "Ada" }; console.log("User: " + user); // User: [object Object]
This is a classic. The + operator with a string operand triggers string coercion.
Better:
jsconsole.log("User:", user); // best for console console.log(`User: ${JSON.stringify(user)}`); // if you need a string
2.2 Template strings
jsconsole.log(`User: ${user}`); // User: [object Object]
Same issue: ${...} coerces to string.
2.3 DOM insertion (innerHTML, textContent)
jsconst div = document.querySelector('#out'); div.textContent = { a: 1 }; // "[object Object]"
If you intended a human-readable rendering, serialize intentionally:
jsdiv.textContent = JSON.stringify({ a: 1 }, null, 2);
Or render in a structured way rather than dumping JSON.
2.4 Logging in some environments (string-only loggers)
Some logging systems or wrappers call String(value) on everything.
Example anti-pattern:
jsfunction log(line) { process.stdout.write(line + "\n"); } log({ event: "login", ok: true }); // [object Object]
Fix: accept multiple arguments or stringify objects.
2.5 Error messages and exceptions
You might see it when throwing or concatenating errors:
jsthrow new Error("Request failed: " + { status: 500 }); // Error: Request failed: [object Object]
Better:
jsthrow new Error("Request failed: " + JSON.stringify({ status: 500 }));
Even better: attach metadata as a property rather than embedding into the message:
jsconst err = new Error("Request failed"); err.status = 500; throw err;
3. How to inspect objects properly (browser + Node)
3.1 Use console.log(obj) (not concatenation)
In browsers and Node, console.log can display object structures when passed as a separate argument.
jsconsole.log("User:", user);
In Chrome DevTools, objects are often displayed by reference—meaning if the object later mutates, expanding it in the console might show the latest state, not the state at log time.
To capture a snapshot:
jsconsole.log("User snapshot:", structuredClone(user)); // or console.log("User snapshot:", JSON.parse(JSON.stringify(user)));
(structuredClone is preferable when available, but can still fail on certain types.)
3.2 console.dir for deep object inspection
console.dir is particularly useful for inspecting objects (especially DOM nodes) in a more navigable way.
jsconsole.dir(document.body);
In Node:
jsconsole.dir(obj, { depth: null, colors: true });
3.3 Node.js: util.inspect
Node’s util.inspect provides robust formatting.
jsimport util from 'node:util'; console.log(util.inspect(obj, { depth: null, colors: true, maxArrayLength: null }));
Why it matters: JSON.stringify drops functions, symbols, and non-enumerable properties, and it fails on circular references by default.
3.4 Debugging tip: check the call site
When you see "[object Object]" in a UI or log line, the key question is:
Where did the implicit string conversion happen?
Search for:
+ something- Template literals:
`${something}` .textContent = something.innerHTML = something- Logging wrappers calling
String()
In a large codebase, using ripgrep:
bashrg "\+\s*[^;]*" src/ rg "\$\{[^}]+\}" src/
Then narrow down suspects where the operand could be an object.
4. Serialization options: what to use and when
4.1 JSON.stringify: best for data interchange
jsJSON.stringify({ a: 1, b: 2 }); // "{\"a\":1,\"b\":2}"
Pretty print:
jsJSON.stringify(obj, null, 2);
Limitations:
- Drops
undefined, functions, and symbols - Converts
Dateto ISO string (viatoJSON) - Fails on circular references
- Loses prototypes and class instances (serializes enumerable properties only)
4.2 Handling circular references
Circular refs are common in real-world objects (DOM nodes, framework state, complex graphs).
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
You have options:
Option A: Use a “safe stringify” replacer
jsfunction safeStringify(value, space = 2) { 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; }, space); } console.log(safeStringify(a));
Option B: In Node, prefer util.inspect for logging
It handles circular refs gracefully.
4.3 toString() overrides and Symbol.toPrimitive
For your own classes, you can make string conversion meaningful.
jsclass User { constructor(id, name) { this.id = id; this.name = name; } toString() { return `User(${this.id}, ${this.name})`; } } const u = new User(1, 'Ada'); console.log('User: ' + u); // User: User(1, Ada)
Or implement Symbol.toPrimitive for better control:
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" +m; // 1234
This is a powerful technique, but use it carefully: implicit coercion can make debugging harder if overused.
5. Framework-specific scenarios
5.1 React: rendering an object directly
React will throw in many cases if you try to render an object as a child:
jsxexport function Profile({ user }) { return <div>{user}</div>; // often: "Objects are not valid as a React child" }
But you might still end up with "[object Object]" if you do something like:
jsxreturn <div>{String(user)}</div>; // or return <div>{`User: ${user}`}</div>;
Preferred patterns:
- Render specific fields:
jsxreturn <div>{user.name} ({user.id})</div>;
- For debugging-only:
jsxreturn <pre>{JSON.stringify(user, null, 2)}</pre>;
5.2 Angular: template interpolation
Angular interpolation converts values to strings.
html<div>{{ user }}</div>
If user is an object, you’ll commonly see [object Object].
Use the built-in json pipe:
html<pre>{{ user | json }}</pre>
5.3 Vue: template rendering
Vue’s mustache syntax stringifies values.
html<div>{{ user }}</div>
Use:
html<pre>{{ JSON.stringify(user, null, 2) }}</pre>
Or render fields explicitly.
6. Logging best practices (the “real” fix in production)
The most expensive place to discover "[object Object]" is in production logs—when the context you needed was discarded.
6.1 Prefer structured logging over string concatenation
Bad:
jslogger.info('request=' + req.body);
Good:
jslogger.info({ body: req.body }, 'incoming request');
This depends on your logger. Many Node loggers (like pino) are designed for structured JSON logs.
6.2 Logger comparisons (practical perspective)
Console (built-in)
- Pros: available everywhere, good for local debugging
- Cons: unstructured, inconsistent formatting across environments
winston
- Pros: flexible transports, widely used
- Cons: structured logging requires more configuration; easy to end up with stringified messages
pino
- Pros: fast, JSON-first structured logs, good ecosystem
- Cons: needs tooling (pretty printer) for local readability
A pino example:
jsimport pino from 'pino'; const logger = pino({ level: 'info' }); logger.info({ user: { id: 1 } }, 'user logged in');
This avoids [object Object] because the logger keeps objects as objects, not strings.
6.3 Redaction and safe logging
Once you start logging objects properly, you risk leaking secrets.
Best practices:
- Redact
password,token,authorization,cookie,secret - Avoid logging full request/response bodies in production
- Use allowlists rather than blocklists when possible
Example: redacting with pino:
jsconst logger = pino({ redact: { paths: ['req.headers.authorization', 'req.headers.cookie', 'user.password'], remove: true } });
6.4 Don’t “fix” it by calling .toString() everywhere
A common mistake is to “solve” [object Object] by doing this:
jslogger.info(obj.toString());
That simply locks in the least informative representation.
The fix is to:
- log the object separately (structured)
- serialize intentionally (JSON, inspect)
- render a subset of fields
7. Debugging techniques when the output is already lost
Sometimes you’re staring at a log line that already contains [object Object]. You can’t recover the original content, but you can still find the source.
7.1 Trace the formatting layer
Typical culprits:
- A homegrown logger wrapper
- A
formatMessage()function - A
printf-style formatter - Telemetry SDK that calls
String()on metadata
Search for:
jsString(value) value + '' '' + value `${value}`
7.2 Use stack traces for unexpected stringification
In Node, you can temporarily monkey-patch Object.prototype.toString to detect unexpected calls (use only locally; never ship this).
jsconst original = Object.prototype.toString; Object.prototype.toString = function () { if (this && this.constructor === Object) { console.trace('toString called on plain object'); } return original.call(this); };
This can help identify unexpected coercion paths. Be cautious: patching builtins can have side effects.
7.3 Add assertions / type guards near boundaries
If a function expects a string and you often pass objects, add a guard:
tsfunction setStatus(text: string) { if (typeof text !== 'string') { throw new TypeError(`setStatus expected string, got ${typeof text}`); } // ... }
Or in plain JS:
jsfunction ensureString(x, name = 'value') { if (typeof x !== 'string') { throw new TypeError(`${name} must be string; got ${Object.prototype.toString.call(x)}`); } return x; }
This prevents silent degradation into [object Object].
8. Patterns to prevent [object Object] in code reviews
8.1 Never concatenate unknown values into strings
If a variable might not be a string, avoid concatenation:
js// Avoid log('payload=' + payload); // Prefer log('payload=', payload); // or log({ payload }, 'payload');
8.2 Provide explicit formatters
Create dedicated formatting helpers:
jsexport function formatForLog(value) { if (typeof value === 'string') return value; if (value instanceof Error) { return value.stack || value.message; } return safeStringify(value, 2); }
Then:
jslogger.info(formatForLog(payload));
8.3 Use TypeScript to catch it early
TypeScript helps prevent passing objects where strings are expected.
tsfunction greet(name: string) { return `Hello ${name}`; } greet({ first: 'Ada' }); // compile-time error
It doesn’t eliminate runtime coercion entirely (because you can still do String(x)), but it reduces accidental misuse across codebases.
8.4 Linting rules
Consider lint rules to discourage unsafe string building.
- ESLint: prefer template literals? (This can be counterproductive if it encourages
${obj}.) - Better: custom lint rule or code review guideline: do not interpolate unknown objects.
Even without a custom rule, you can enforce patterns such as “logger calls must be structured.”
9. Practical recipes (copy/paste friendly)
Recipe A: Print a debug view in Node that never shows [object Object]
jsimport util from 'node:util'; export function debug(value, label = 'debug') { const out = util.inspect(value, { depth: null, colors: process.stdout.isTTY, maxArrayLength: 100, breakLength: 120 }); console.log(`${label}: ${out}`); }
Recipe B: Safe JSON stringify for web apps
jsexport function safeStringify(value, space = 2) { const seen = new WeakSet(); return JSON.stringify(value, (key, val) => { if (typeof val === 'bigint') return val.toString(); if (typeof val === 'object' && val !== null) { if (seen.has(val)) return '[Circular]'; seen.add(val); } return val; }, space); }
Notes:
- Handles
BigInt(JSON can’t serialize it natively) - Prevents crashes on circular refs
Recipe C: Express middleware that logs requests safely
jsimport pino from 'pino'; const logger = pino({ redact: { paths: ['req.headers.authorization', 'req.headers.cookie'], remove: true } }); export function requestLogger(req, res, next) { logger.info({ req: { method: req.method, url: req.originalUrl, headers: req.headers, // consider limiting body logging or sampling body: req.body } }, 'incoming request'); next(); }
This prevents the string-coercion trap entirely.
10. Mental model: treat stringification as an API boundary
A consistent way to avoid [object Object] is to treat stringification as something you do only at boundaries:
- UI boundary: render fields or explicitly format JSON
- Log boundary: structured logs or explicit serialization
- Network boundary: JSON with schema validation
- Error boundary: structured metadata + message
Inside your application logic, keep objects as objects.
If you do need a string representation, be explicit and intentional about it:
- For humans: pretty JSON or
util.inspect - For machines: strict JSON (possibly with schemas)
- For identifiers: format a stable, short string (e.g.,
User(123))
11. Checklist: when you see [object Object], do this
- Find where string coercion happened
- Look for
+, template literals,String(), DOM assignments, logger formatting.
- Look for
- Switch to structured logging or multi-arg
console.logconsole.log('label:', obj)
- Pick the right serializer
JSON.stringify(obj, null, 2)for datasafeStringifyfor circular refsutil.inspectin Node for rich debug output
- Avoid leaking secrets
- Add redaction rules and field allowlists
- Prevent recurrence
- Add type checks, better logger APIs, and code review rules
Closing perspective
"[object Object]" is a symptom, not the disease. The disease is losing structure—usually because an object crossed a boundary that expected a string.
Once you internalize that, the fixes become systematic:
- don’t concatenate unknown values
- log objects as objects
- serialize explicitly and safely
- adopt structured logging and redaction
That combination eliminates the debugging dead-end and improves both developer experience and production observability.
