You’ve seen it in a UI, a toast message, a log line, a test failure, or an alert box:
[object Object]
It’s one of the most common “why is my app doing this?” moments in JavaScript development. It usually appears when you expected meaningful data (like a user name, a validation error, or an API response), but instead got the default string representation of an object.
This article explains why [object Object] happens, how to quickly identify where it’s coming from, and how to fix it across typical stacks (Node.js, React, Vue, Angular, Express, Next.js, etc.). We’ll cover practical debugging steps, serialization patterns, common pitfalls with string concatenation and interpolation, safe logging strategies, and best practices for designing error and data shapes so this doesn’t reappear.
1) What [object Object] Actually Means
In JavaScript, most things can be coerced to a string. When a plain object is coerced into a string without a custom converter, JavaScript uses the object’s default toString() behavior:
jsconst obj = { a: 1 }; String(obj); // "[object Object]" obj.toString(); // "[object Object]" '' + obj; // "[object Object]" `${obj}`; // "[object Object]"
Why that exact value?
Object.prototype.toString.call(obj)returns a string like"[object Object]"where the second word is the internal “class” (for arrays it would be"[object Array]", for dates"[object Date]", etc.).- For a plain object, it’s
Object.
So [object Object] is not “an error” by itself. It’s a symptom: somewhere, an object got implicitly converted into a string.
Quick mental model
If you see [object Object], you likely did one of these:
- Concatenated an object with a string (
'Error: ' + err) - Used template literals with an object (
${err}) - Assigned an object to a string-only sink (DOM text, toast library message, HTML attribute, query string, etc.)
2) The Most Common Root Causes (With Fixes)
Cause A: String concatenation / interpolation
jsconst user = { id: 1, name: 'Ada' }; console.log('User: ' + user); // "User: [object Object]" console.log(`User: ${user}`); // "User: [object Object]"
Fix: pick a property, or stringify.
jsconsole.log('User: ' + user.name); console.log('User:', user); // best for logs console.log('User: ' + JSON.stringify(user));
Prefer console.log('User:', user) rather than concatenation. DevTools prints objects as expandable structures, which is much more useful.
Cause B: Rendering an object directly in the UI
React
jsxexport function Profile({ user }) { return <div>{user}</div>; // renders [object Object] }
Fix: render a string property or format it.
jsxreturn <div>{user.name}</div>;
If you truly want to render JSON for debugging:
jsxreturn <pre>{JSON.stringify(user, null, 2)}</pre>;
Vue
vue<template> <div>{{ user }}</div> <!-- often becomes [object Object] --> </template>
Fix:
vue<div>{{ user.name }}</div> <pre>{{ JSON.stringify(user, null, 2) }}</pre>
Angular
Angular typically shows [object Object] when interpolating objects:
html<div>{{ user }}</div>
Fix using built-in json pipe:
html<pre>{{ user | json }}</pre>
Or render specific fields.
Cause C: Passing an object to an error/toast/snackbar library
Many UI notification libraries expect a string message.
jstoast.error(err); // err is an object => “[object Object]”
Fix: normalize the error into a user-facing string.
jstoast.error(getErrorMessage(err)); function getErrorMessage(err) { if (!err) return 'Unknown error'; if (typeof err === 'string') return err; // Common patterns if (err instanceof Error) return err.message; if (typeof err.message === 'string') return err.message; // Axios/fetch API shapes if (err.response?.data?.message) return err.response.data.message; if (err.data?.message) return err.data.message; // Last resort return JSON.stringify(err); }
For user-facing messages, you usually don’t want raw JSON. But for debugging, JSON.stringify is better than [object Object].
Cause D: Query strings / URLs built from objects
jsconst filters = { sort: 'name', page: 2 }; const url = '/api/users?filters=' + filters; // /api/users?filters=[object Object]
Fix: encode properly.
- If the API expects separate parameters:
jsconst params = new URLSearchParams({ sort: 'name', page: '2' }); fetch(`/api/users?${params.toString()}`);
- If the API expects a JSON blob (less common, but used sometimes):
jsconst params = new URLSearchParams({ filters: JSON.stringify(filters) }); fetch(`/api/users?${params}`);
Cause E: FormData misuse
jsconst payload = { name: 'Ada', age: 37 }; const fd = new FormData(); fd.append('payload', payload); // becomes “[object Object]”
Fix:
jsfd.append('payload', JSON.stringify(payload));
Or append each field individually.
3) Debugging: Finding Where the Coercion Happens
When you see [object Object], the key question is: Where did the object become a string?
Step 1: Search for concatenation and interpolation
Search in your codebase for:
+ something`${something}`.textContent =.innerText =- toast/snackbar calls
new Error(something)(people often pass non-strings)res.send(something)in Express whensomethingmight not be what you think
Step 2: Use typeof and structural logging
Avoid:
jsconsole.log('err=' + err);
Prefer:
jsconsole.log('err is:', err); console.log('typeof err:', typeof err); console.log('keys:', err && Object.keys(err));
Step 3: Break on DOM changes (front-end)
In Chrome DevTools:
- Inspect the element showing
[object Object]. - In the Elements panel, right-click → Break on… → Subtree modifications.
- Reproduce the issue.
- DevTools will pause at the code that mutated the DOM.
This is extremely effective when [object Object] appears in a specific spot in the UI but you don’t know which code path rendered it.
Step 4: Add guardrails with assertions
In TypeScript or even plain JS, add runtime checks at boundaries:
jsfunction assertString(value, name) { if (typeof value !== 'string') { throw new TypeError(`${name} must be a string, got ${Object.prototype.toString.call(value)}`); } } assertString(toastMessage, 'toastMessage');
This turns a silent coercion into a loud, actionable error.
4) Serialization Options: console.log vs JSON.stringify vs Inspectors
console.log(obj)
Best for interactive debugging. You can expand nested fields. Note: some consoles show objects by reference; values might appear “updated” if the object later mutates.
JSON.stringify(obj)
Useful for:
- Logging to text-only systems
- Storing in localStorage
- Sending over network
But it has pitfalls:
- Fails on circular references
- Drops
undefined, functions, symbols - Dates become ISO strings only if you explicitly convert (otherwise they stringify to ISO via
toJSON, which is usually fine)
Example:
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
structuredClone and util.inspect
In Node.js, for logging:
jsimport util from 'node:util'; console.log(util.inspect(obj, { depth: 5, colors: true }));
This handles more types than JSON and is friendlier for server logs.
Safe stringify for circular structures
Use a library:
fast-safe-stringifyjson-stringify-safe
Or implement a small replacer:
jsfunction safeStringify(value) { const seen = new WeakSet(); return JSON.stringify(value, (k, v) => { if (typeof v === 'object' && v !== null) { if (seen.has(v)) return '[Circular]'; seen.add(v); } return v; }, 2); }
5) Error Objects: Why They Often Turn Into [object Object]
A huge portion of [object Object] sightings come from mishandled errors.
Problem: Error doesn’t stringify the way you expect
jsconst e = new Error('DB down'); String(e); // "Error: DB down" (okay) JSON.stringify(e); // "{}" (surprising)
Error properties like message and stack are often non-enumerable, so JSON serialization loses them.
Fix: normalize error serialization
jsfunction serializeError(err) { if (!err) return { message: 'Unknown error' }; if (typeof err === 'string') return { message: err }; if (err instanceof Error) { return { name: err.name, message: err.message, stack: err.stack, cause: err.cause }; } // Axios-like if (err.isAxiosError) { return { name: err.name, message: err.message, status: err.response?.status, data: err.response?.data }; } // Plain object fallback return { message: err.message || 'Unknown error', ...err }; }
Then for logs:
jslogger.error({ err: serializeError(err) }, 'Request failed');
Tooling: prefer structured loggers
- pino: very fast, JSON logs, great for production
- winston: flexible transports, more overhead
- bunyan: classic JSON logger
Structured logging avoids accidental coercion because you attach objects as fields rather than concatenating strings.
Example with pino:
jsimport pino from 'pino'; const logger = pino(); logger.error({ err }, 'Operation failed');
6) API Responses and Fetch/Axios Pitfalls
Fetch: forgetting to await response.json()
A common bug is rendering a Response object or a pending promise.
jsconst res = await fetch('/api/user'); setUser(res); // Response object -> can show up as [object Object]
Fix:
jsconst res = await fetch('/api/user'); const user = await res.json(); setUser(user);
Axios: using the whole response rather than response.data
jsconst response = await axios.get('/api/user'); setUser(response); // big response object
Fix:
jssetUser(response.data);
7) Preventing [object Object] with TypeScript and Better Types
TypeScript won’t magically prevent coercion, but it helps at boundaries.
Example: enforce string messages for UI notifications
tstype Toast = { error: (msg: string) => void; }; declare const toast: Toast; toast.error({ message: 'nope' }); // TypeScript error: argument is not a string
Discriminated unions for API errors
Define an error shape your UI knows how to display:
tstype ApiError = | { kind: 'Validation'; message: string; fields: Record<string, string> } | { kind: 'Auth'; message: string } | { kind: 'Unknown'; message: string; debugId?: string }; function toUserMessage(err: ApiError): string { return err.message; }
Now you avoid passing arbitrary objects around as “messages”.
8) UI Rendering Best Practices (Framework-Agnostic)
Principle: don’t render raw objects
Make formatting explicit:
- UI components should accept primitives (string/number) where possible
- If a component accepts an object, it should be responsible for rendering meaningful fields
Bad:
jsx<Toast message={error} />
Better:
jsx<Toast message={getErrorMessage(error)} />
Provide a developer-only JSON view
For admin panels or internal tools, a JSON viewer component helps.
React example:
jsxfunction JsonDebug({ value }) { return ( <details> <summary>Debug</summary> <pre>{JSON.stringify(value, null, 2)}</pre> </details> ); }
In production apps, gate it behind an environment flag.
9) Advanced: Custom toString, toJSON, and Symbol.toPrimitive
You can control how objects stringify, but use this carefully—especially for domain objects.
toString()
jsclass User { constructor(id, name) { this.id = id; this.name = name; } toString() { return `User(${this.id}, ${this.name})`; } } const u = new User(1, 'Ada'); String(u); // "User(1, Ada)"
This can reduce [object Object], but it can also hide important data if used indiscriminately.
toJSON()
Controls JSON serialization:
jsclass User { constructor(id, name, passwordHash) { this.id = id; this.name = name; this.passwordHash = passwordHash; } toJSON() { return { id: this.id, name: this.name }; } } JSON.stringify(new User(1, 'Ada', '...')); // {"id":1,"name":"Ada"}
This is useful for APIs to avoid leaking secrets.
Symbol.toPrimitive
Fine-grained coercion 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(199); '' + m; // "$1.99" +m; // 1.99
Use sparingly; it can make code surprising.
10) Security and Privacy Considerations
A common “fix” is to stringify everything. That can backfire:
- You might log tokens, passwords, session cookies
- You might expose internal server error details to end users
Best practice: separate user messages from debug payloads
- User message: minimal, actionable, non-sensitive
- Debug details: structured logs, correlation IDs, stack traces (server-side)
Example:
jsconst debugId = crypto.randomUUID(); logger.error({ debugId, err: serializeError(err) }, 'Payment failed'); return res.status(500).json({ message: 'Payment could not be processed right now.', debugId });
This avoids showing [object Object] and avoids dumping internals.
11) Tool Comparisons and Practical Tips
Logging
- console: fine locally, weak in production
- pino: fast, structured JSON, great with log pipelines
- winston: flexible transports, heavier
Inspecting objects
- Chrome DevTools: excellent interactive inspection, breakpoints, DOM break-on
- Node inspect / util.inspect: better than JSON for many server objects
- VS Code debugger: watch expressions, variable explorer
Viewing JSON
- Browser extensions / built-in viewers
jqin the terminal
bashcat app.log | jq '.'
12) A Checklist to Eliminate [object Object] Systematically
- Locate where it appears (UI element, log line, test output).
- Trace the value backward (break on DOM changes, add logging, use debugger).
- Find the coercion point: concatenation, template literal, message sink.
- Decide intent:
- User-facing string? Extract a property or map the object to a message.
- Debug output? Use structured logging or JSON formatting.
- Add type/runtime boundaries (TypeScript types, assertion helpers).
- Harden error handling (normalize errors, avoid leaking sensitive data).
- Add tests for message formatting and error rendering.
13) End-to-End Example: From Bug to Fix
The bug
A React app calls an API and shows a toast on failure:
jsxasync function saveProfile(profile) { try { await api.updateProfile(profile); toast.success('Saved!'); } catch (err) { toast.error('Save failed: ' + err); } }
If err is { message: 'Email invalid' }, you get:
Save failed: [object Object]
The fix
jsxfunction getErrorMessage(err) { if (!err) return 'Unknown error'; if (typeof err === 'string') return err; if (err instanceof Error) return err.message; if (typeof err.message === 'string') return err.message; return 'Unexpected error'; } async function saveProfile(profile) { try { await api.updateProfile(profile); toast.success('Saved!'); } catch (err) { toast.error(`Save failed: ${getErrorMessage(err)}`); console.error('Save failed details:', err); } }
Now:
- Users see meaningful text
- Developers get the full object in the console
- No more
[object Object]
14) Summary
[object Object] appears when an object gets implicitly converted to a string. Fixing it is usually straightforward once you identify the coercion site:
- Don’t concatenate objects into strings
- Render specific fields, not whole objects
- Normalize errors into user-facing messages
- Use structured logs for debugging
- Use
JSON.stringify(or safer variants) when you truly need string serialization - Add type and runtime guardrails at boundaries
When treated as a signal rather than a mystery, [object Object] becomes a fast, teachable debugging win—and a reminder to keep your data shapes and UI contracts explicit.
![Diagnosing and Fixing “[object Object]”: A Practical Guide for JavaScript and Web Engineers](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_e93dd33c1e.png)