Why you’re seeing “[object Object]”
If you’ve ever rendered a value in the UI, concatenated it into a string, or logged something and gotten the infamous:
[object Object]
…you’ve hit a fundamental JavaScript behavior: objects don’t automatically stringify into useful text. Instead, JavaScript falls back to the default Object.prototype.toString() implementation.
This article explains:
- What “[object Object]” actually is and where it comes from
- The most common places it appears (DOM/UI, logs, HTTP, templating)
- How to correctly inspect objects in Node.js and browsers
- Safe serialization strategies (including circular references and BigInt)
- Debugging techniques and best practices for production systems
- Tooling comparisons and patterns that scale
The goal is to make you confident diagnosing the root cause and choosing the right fix—whether you’re a junior developer trying to display data, or a senior engineer building observability pipelines.
The core mechanics: implicit string conversion
In JavaScript, when you do any operation that expects a string—like string concatenation, template interpolation, DOM text assignment in certain contexts, or some logging calls—JavaScript performs type coercion.
Consider:
jsconst obj = { a: 1 }; console.log('Value: ' + obj);
The + operator with a string triggers string conversion. For objects, that usually means:
- Try
obj[Symbol.toPrimitive]('string')if present - Else try
obj.toString() - Else try
obj.valueOf()
Default toString() for plain objects returns:
jsObject.prototype.toString.call({}) // "[object Object]"
That string is formed as:
"[object "+ internal class +"]"- For plain objects, that internal class is typically
Object
So “[object Object]” is not an error by itself—it’s a symptom that you’re implicitly stringifying an object without a custom string representation.
Common places it appears (and what’s happening)
1) UI rendering (React, Vue, Angular, templating)
React
jsxfunction UserCard({ user }) { return <div>{user}</div>; }
React will not render raw objects as text; depending on the situation, it may throw (e.g., “Objects are not valid as a React child”) or you may see [object Object] if it’s coerced somewhere in your code.
Correct approaches:
jsxreturn <div>{user.name}</div>;
Or for debugging:
jsxreturn <pre>{JSON.stringify(user, null, 2)}</pre>;
Vue
html<div>{{ user }}</div>
Vue will call toString() on objects in interpolation, yielding [object Object].
Fix:
html<div>{{ user.name }}</div> <pre>{{ JSON.stringify(user, null, 2) }}</pre>
2) String concatenation and template literals
jsconst payload = { ok: true }; console.log(`payload=${payload}`); // payload=[object Object]
Template literals also coerce to string. Use:
jsconsole.log(`payload=${JSON.stringify(payload)}`);
3) Network calls: query strings and form encoding
If you do something like:
jsconst params = new URLSearchParams({ filter: { a: 1 } }); params.toString();
The object becomes "[object Object]" because URLSearchParams expects string values.
Fix by encoding JSON explicitly:
jsconst params = new URLSearchParams({ filter: JSON.stringify({ a: 1 }) });
Or flatten appropriately:
jsconst params = new URLSearchParams({ 'filter.a': '1' });
4) DOM APIs
jsdocument.querySelector('#out').textContent = { a: 1 }; // textContent expects a string; object coerces -> [object Object]
Fix:
jsdocument.querySelector('#out').textContent = JSON.stringify({ a: 1 }, null, 2);
5) Logging: console differences across environments
In browsers, console.log(obj) usually prints an expandable object, not [object Object].
But these do stringify:
jsconsole.log('obj=' + obj); console.log(`obj=${obj}`);
In Node.js, console.log(obj) prints a structured representation. Yet concatenation still triggers [object Object].
The right solution depends on your goal
Before “fixing” [object Object], decide what you need:
- Human-friendly inspection (debugging)
- Stable serialization (storage, hashing, caching)
- User-facing display (UI)
- Machine-oriented transport (HTTP payloads)
Each requires different choices.
Debugging and inspection options (practical comparisons)
Option A: console.log(obj) (best default for debugging)
jsconsole.log(obj);
- Browser devtools: interactive, expandable
- Node: uses
util.inspectunder the hood - Caveat: some consoles show objects by reference; expanding later may show updated values (common in browsers)
Technique: snapshot objects to freeze the view:
jsconsole.log(JSON.parse(JSON.stringify(obj)));
This loses functions, undefined, symbols, circular refs, and special types—so it’s strictly a debugging snapshot.
Option B: console.dir(obj, { depth: null })
In Node:
jsconsole.dir(obj, { depth: null, colors: true });
- Shows nested structures without truncation
- More controllable than
console.log
In browsers, console.dir prints an object-focused view.
Option C: JSON.stringify(obj, null, 2)
jsconsole.log(JSON.stringify(obj, null, 2));
Pros:
- Familiar, portable
- Produces a string you can persist or send
Cons:
- Fails on circular references
- Drops
undefined, functions, symbols BigIntthrows- Converts
Dateto ISO string (viatoJSON)
Option D: Node’s util.inspect
jsimport util from 'node:util'; console.log(util.inspect(obj, { depth: null, colors: true, maxArrayLength: 100, breakLength: 120, }));
Pros:
- Handles circular references
- More faithful representation than JSON
Cons:
- Not a standardized wire format
- Output not stable across Node versions/settings
Option E: Browser structuredClone for safe cloning
If you want a deep copy that supports many built-in types:
jsconst copy = structuredClone(obj);
Pros:
- Supports
Map,Set,Date, typed arrays, etc.
Cons:
- Still not a string
- Functions can’t be cloned
Serialization: turning objects into strings safely
The baseline: JSON
For APIs and storage, JSON is often the correct choice.
jsconst s = JSON.stringify({ a: 1 }); const o = JSON.parse(s);
But real systems hit edge cases quickly.
Handling circular references
This breaks:
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
Approach 1: Use a library like flatted
flatted can encode circular references in a JSON-like format.
Example (conceptual):
jsimport { stringify, parse } from 'flatted'; const a = {}; a.self = a; const s = stringify(a); const restored = parse(s);
Pros:
- Simple
- Handles cycles
Cons:
- Not interoperable with standard JSON parsers
- Use carefully for network protocols unless both sides agree
Approach 2: Custom replacer with a WeakSet
If you simply want to drop cycles during logging:
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; }, 2); } console.log(safeStringify(a));
Pros:
- Still standard JSON
- Great for logs
Cons:
- Loses reference identity
- Not a true round-trip
BigInt, Dates, Maps/Sets: common “why is JSON lying to me?” issues
BigInt
jsJSON.stringify({ n: 1n }); // TypeError
Fix with a replacer:
jsfunction stringifyWithBigInt(value) { return JSON.stringify(value, (k, v) => typeof v === 'bigint' ? v.toString() : v ); }
If you need round-trip, you must encode type info:
jsfunction stringifyTyped(value) { return JSON.stringify(value, (k, v) => { if (typeof v === 'bigint') return { $type: 'bigint', value: v.toString() }; if (v instanceof Date) return { $type: 'date', value: v.toISOString() }; if (v instanceof Map) return { $type: 'map', value: [...v.entries()] }; if (v instanceof Set) return { $type: 'set', value: [...v.values()] }; return v; }); } function parseTyped(text) { return JSON.parse(text, (k, v) => { if (v && v.$type === 'bigint') return BigInt(v.value); if (v && v.$type === 'date') return new Date(v.value); if (v && v.$type === 'map') return new Map(v.value); if (v && v.$type === 'set') return new Set(v.value); return v; }); }
This pattern is common in caches and message buses when you can’t use a richer binary format.
Dates
JSON.stringify(new Date()) yields an ISO string because Date.prototype.toJSON returns toISOString().
That’s usually good, but be explicit in contracts: is it UTC? Which format? Are milliseconds included?
Maps and Sets
JSON drops them into {} by default because their properties are not enumerable entries.
Use conversion:
jsJSON.stringify({ m: [...myMap.entries()] }); JSON.stringify({ s: [...mySet.values()] });
Or use the typed approach above.
UI best practices: don’t dump objects blindly
Render specific fields
Users typically need curated values, not raw objects.
React example:
jsxfunction OrderSummary({ order }) { return ( <section> <div>Order ID: {order.id}</div> <div>Total: {order.total.currency} {order.total.amount}</div> <div>Status: {order.status}</div> </section> ); }
Provide a “debug panel” in development only
jsxfunction DebugJSON({ value }) { if (process.env.NODE_ENV !== 'development') return null; return <pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(value, null, 2)}</pre>; }
Avoid calling JSON.stringify on every render for big objects
For performance, memoize:
jsxconst pretty = useMemo(() => JSON.stringify(value, null, 2), [value]);
Or better: stringify only on demand (toggle/open).
Logging best practices: preventing [object Object] and improving observability
Prefer structured logging over string concatenation
Bad:
jslogger.info('user=' + user + ' action=' + action);
Good:
jslogger.info({ userId: user.id, action }, 'User action');
Most modern loggers (Pino, Winston, Bunyan, Serilog equivalents) support JSON logs.
Example with Pino (Node.js)
jsimport pino from 'pino'; const logger = pino(); logger.info({ user: { id: 123, role: 'admin' } }, 'Authenticated');
This yields a JSON log line you can query in Elasticsearch, Loki, Datadog, etc.
Include correlation IDs
[object Object] is often discovered while debugging distributed requests; correlation IDs make logs usable.
jslogger.info({ requestId, userId }, 'Request started');
Know when to redact
Dumping objects may leak secrets.
- Redact passwords, tokens, cookies
- Avoid logging full request bodies by default
- Use logger redaction features if available
Pino example:
jsconst logger = pino({ redact: { paths: ['req.headers.authorization', 'password', 'token'], censor: '[REDACTED]' } });
Advanced: customizing how your objects stringify
Implement toString()
For domain objects, you can provide a meaningful string:
jsclass User { constructor(id, email) { this.id = id; this.email = email; } toString() { return `User(${this.id}, ${this.email})`; } } const u = new User(1, 'a@b.com'); console.log('u=' + u); // u=User(1, a@b.com)
Be cautious:
toString()should be cheap- Avoid including secrets
Implement Symbol.toPrimitive
This is the most precise control over coercion:
jsclass Money { constructor(amount, currency) { this.amount = amount; this.currency = currency; } [Symbol.toPrimitive](hint) { if (hint === 'string') return `${this.currency} ${this.amount}`; return this.amount; } } const m = new Money(10, 'USD'); console.log(String(m)); // "USD 10" console.log(m + 5); // 15 (numeric coercion)
This can eliminate [object Object] while preserving intuitive behavior.
Implement toJSON() for JSON.stringify
JSON.stringify checks for toJSON.
jsclass User { constructor(id, email, passwordHash) { this.id = id; this.email = email; this.passwordHash = passwordHash; } toJSON() { return { id: this.id, email: this.email }; } } JSON.stringify(new User(1, 'a@b.com', '...')); // {"id":1,"email":"a@b.com"}
This is a powerful way to:
- Provide stable wire representations
- Redact sensitive internal fields
But note: it affects every JSON serialization, which may surprise other parts of the system.
Debugging checklist when you see “[object Object]”
-
Find where string coercion happens
- Search for
+ obj,`${obj}`,.textContent = obj, query string builders
- Search for
-
Log the raw object without concatenation
console.log(obj)orconsole.dir(obj, { depth: null })
-
Confirm the type
jsconsole.log(typeof obj); // "object" console.log(obj?.constructor?.name); console.log(Object.prototype.toString.call(obj));
-
Check for unexpected nested objects
- E.g., you thought
user.namewas a string but it’s{ first, last }
- E.g., you thought
-
Watch for proxies and getters
- Framework state objects may be proxies; serializing them can behave differently
-
If JSON.stringify fails
- Detect circulars
- Identify BigInt
- Inspect Maps/Sets
-
Fix by choosing the right representation
- UI: render specific fields
- Logs: structured logging
- Network: JSON payloads, not query string objects
Tooling and workflow tips
Browser devtools
- Use
console.log(obj)rather than concatenation - Use
console.table(arrayOfObjects)for tabular data - Use breakpoints + “Scope” panel to inspect objects live
Node.js debugging
- Use
node --inspectand Chrome DevTools - Use
util.inspectfor deep objects - Consider
pino-prettyin dev if you want readable logs while keeping JSON in prod
Linters and code review heuristics
You can prevent many [object Object] occurrences by discouraging string concatenation in logs.
- ESLint rules (team conventions): prefer logger calls with objects
- Code review: flag
"..." + objpatterns unless explicitly intended
Patterns that scale in production
1) Always send JSON over the wire (when you control both ends)
Instead of:
GET /search?filter=[object%20Object]
Prefer:
POST /searchwith JSON body- or
GET /search?filter={...}only if you explicitlyencodeURIComponent(JSON.stringify(filter))and have a clear contract
2) Standardize logging format
- JSON logs
- consistent keys (
requestId,userId,service,env) - redaction
- size limits (avoid huge payload dumps)
3) Use stable stringification when hashing/cache keys matter
If you generate cache keys from objects, plain JSON.stringify can be unstable because key order may vary based on construction.
Use a stable stringify approach (library conceptually: “stable-json-stringify”) so:
{a:1,b:2}and{b:2,a:1}produce the same string
This is important in memoization, caching layers, and idempotency keys.
4) Don’t rely on toString() for business logic
Custom toString() is useful for debugging, but business logic should use explicit fields.
Quick reference: how to avoid [object Object]
- To display object:
JSON.stringify(obj, null, 2)(or render fields) - To log object:
console.log(obj)or structured loggerlogger.info({ obj }) - To serialize for APIs:
JSON.stringify(obj)with agreed schema - To handle circulars in logs: custom
safeStringifywithWeakSet - To support BigInt: replacer encoding (string) or typed wrappers
- To fix coercion: avoid
"" + objand`${obj}`unlessobjis a string
Conclusion
“[object Object]” is the visible result of implicit string conversion of a JavaScript object using the default toString() implementation. It’s not the root problem; it’s a hint that somewhere in your code you’re using an object where a string is expected.
The best fix is context-dependent:
- For UI: render specific fields or pretty-print JSON in a development panel
- For debugging: log objects directly, use
console.dir/util.inspect, and snapshot when needed - For production logging: adopt structured logging with redaction and correlation IDs
- For serialization: use JSON deliberately, and handle circular references, BigInt, and richer types explicitly
Once you recognize the coercion pathways (+, template literals, DOM setters, query encoders), you’ll be able to track down the exact line producing [object Object] and replace it with a representation that is human-meaningful, machine-safe, and production-friendly.
![Diagnosing and Fixing “[object Object]”: A Practical Guide to JavaScript Object Stringification, Logging, and Serialization](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_436a8e5819.png)