You typed a log statement, opened the browser console, or surfaced an error message to a user—and instead of something useful you got:
[object Object]
This is one of the most common (and most avoidable) JavaScript diagnostics failures. It’s not an “error” by itself; it’s JavaScript telling you it had to turn an object into a string and fell back to the object’s default string representation.
For developers, this often shows up in:
- UI text where you expected a message (React/Vue/Angular templates)
- HTTP error handling (Axios/fetch)
- Server logs (Node.js, Express, Nest)
- Logging pipelines (Winston/Pino/Datadog/Sentry)
- String concatenations in debugging output
This article explains why [object Object] happens, how to pinpoint the exact coercion point, and how to prevent it with robust patterns in JavaScript and TypeScript.
1) What [object Object] actually means
In JavaScript, most values can be converted into strings. When you do any of the following:
- String concatenation:
"Error: " + obj - Template interpolation:
`Error: ${obj}` - Assigning to DOM/text:
element.textContent = obj - Passing to a logging function that internally concatenates
…the engine performs ToString conversion.
For plain objects, if you haven’t defined a custom string conversion, the default is:
jsString({}) // "[object Object]" ({}).toString() // "[object Object]" `${{}}` // "[object Object]" "" + {} // "[object Object]" (with some parsing caveats)
Why that exact text?
Object.prototype.toString()returns a string in the format"[object Type]".- For a plain object,
Typeis"Object".
So [object Object] is effectively: “I’m an Object and nobody told me how to stringify nicely.”
Key implication
If you see [object Object], somewhere a real, structured object got flattened to a string too early (or in the wrong way). Your goal is to keep it structured for logs/telemetry or to format it intentionally for UI.
2) The most common root causes (with examples)
Cause A: String concatenation or template interpolation
jsconst err = { message: "Invalid token", code: 401 }; console.error("Auth failed: " + err); // Auth failed: [object Object] console.error(`Auth failed: ${err}`); // Auth failed: [object Object]
Fix: log the object as a separate argument, or format explicitly.
jsconsole.error("Auth failed:", err); // preserves structure in consoles console.error("Auth failed: %o", err); // console formatting
Cause B: Rendering an object directly in UI
React
jsxexport function Banner({ error }) { return <div className="error">{error}</div>; // if error is an object -> [object Object] }
Fix: render a specific string field, or format.
jsxreturn <div className="error">{error?.message ?? "Something went wrong"}</div>;
If you truly want to show structured data (e.g., dev-only):
jsxreturn <pre>{JSON.stringify(error, null, 2)}</pre>;
Vue
html<div class="error">{{ error }}</div>
Fix:
html<div class="error">{{ error?.message || 'Something went wrong' }}</div>
Cause C: Throwing or rejecting with non-Error objects
JavaScript allows throwing anything:
jsthrow { message: "Nope", code: "E_NOPE" };
This causes downstream handlers to do things like:
jscatch (e) { setStatus(`Failed: ${e}`); // -> [object Object] }
Fix: throw Error (or subclasses) and attach metadata.
jsclass AppError extends Error { constructor(message, { code, cause } = {}) { super(message); this.name = "AppError"; this.code = code; this.cause = cause; } } throw new AppError("Nope", { code: "E_NOPE" });
Cause D: Logging libraries that implicitly stringify
Some loggers handle objects well, others stringify depending on configuration.
console.log("x", obj)generally shows the object.- But
logger.info("x " + obj)will always stringify first.
Fix: pass objects as structured fields:
jslogger.info({ err, userId }, "Auth failed");
This is the canonical style for structured loggers like Pino.
Cause E: Using JSON.stringify incorrectly (or on circular objects)
A typical “fix” is JSON.stringify(obj). That works—until it doesn’t.
- It can throw on circular references.
- It drops functions, symbols, and non-enumerable properties.
- It serializes
Errorobjects poorly (message/stack may be non-enumerable).
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
Fix: use safe stringifiers or inspection tools when needed.
3) Pinpointing the coercion point: debugging workflow
The core question is: where did the object become a string?
Step 1: Search for string concatenation or interpolation
Search your codebase for patterns:
"" ++ error`${error}`toString()usage- UI templates rendering an object
In TypeScript, enable strictness to catch this earlier (more later).
Step 2: Add type/shape logging around the suspected boundary
If you suspect a value is sometimes an object, sometimes a string:
jsconsole.log("typeof error", typeof error); console.log("isError", error instanceof Error); console.log("keys", error && typeof error === "object" ? Object.keys(error) : null); console.log("value", error);
In Node, console.log may not show full depth; use util.inspect:
jsimport util from "node:util"; console.log(util.inspect(error, { depth: 10, colors: true }));
Step 3: Use breakpoints on caught exceptions
In Chrome DevTools:
- Sources → enable Pause on exceptions
- Reproduce the issue
- Inspect the thrown value (
e) and see where it’s created
In Node:
- Run with
node --inspectand attach DevTools - Or use VS Code’s debugger with “Pause on exceptions”
Step 4: Instrument the conversion point
If you strongly suspect a specific formatting function, temporarily harden it:
jsfunction formatErrorForUi(err) { if (err && typeof err === "object") { // force a useful failure rather than silent [object Object] return err.message ?? JSON.stringify(err); } return String(err); }
This turns “mystery coercion” into an explicit decision.
4) Correct ways to log objects (browser vs Node)
Browser console best practices
Prefer:
jsconsole.log("payload", payload); console.error("request failed", err);
Instead of:
jsconsole.log("payload=" + payload); // likely [object Object]
Use formatting specifiers:
jsconsole.log("payload=%o", payload); console.log("payload=%O", payload);
%ooften shows expandable DOM/object with concise formatting.%Oshows a more detailed expandable object.
Node.js best practices
In Node, console.log(obj) is okay for development. For production services, prefer structured logging.
util.inspect
jsimport { inspect } from "node:util"; console.log(inspect(payload, { depth: 6, colors: true, breakLength: 120 }));
Pino (structured, fast)
jsimport pino from "pino"; const logger = pino(); logger.info({ payload, requestId }, "received payload"); logger.error({ err }, "request failed");
Pino treats err specially (serializes message/stack) when you use the err key.
Winston (flexible transports)
Winston can do structured logs, but you must configure formats correctly.
jsimport winston from "winston"; const logger = winston.createLogger({ level: "info", format: winston.format.combine( winston.format.errors({ stack: true }), winston.format.json() ), transports: [new winston.transports.Console()], }); logger.error("failed", { err });
If you do string concatenation before calling Winston, you still lose structure.
5) Handling errors correctly across fetch and Axios
Fetch: errors are not thrown for HTTP 4xx/5xx
A common pitfall is assuming fetch throws on non-2xx:
jsconst res = await fetch(url); if (!res.ok) { throw await res.json(); // throwing plain object => later [object Object] }
Better:
jsclass HttpError extends Error { constructor(message, { status, body } = {}) { super(message); this.name = "HttpError"; this.status = status; this.body = body; } } const res = await fetch(url); if (!res.ok) { let body; try { body = await res.json(); } catch { body = await res.text(); } throw new HttpError("Request failed", { status: res.status, body }); }
Now downstream code can safely do err.message, and logs can include err.body.
Axios: error objects are rich but nested
Axios throws an AxiosError that includes response, request, config.
Bad:
jscatch (err) { toast.error("Request failed: " + err); // [object Object] depending on coercion }
Better:
jsimport axios from "axios"; function getAxiosMessage(err) { if (axios.isAxiosError(err)) { return err.response?.data?.message ?? err.message ?? "Request failed"; } return err instanceof Error ? err.message : String(err); } catch (err) { toast.error(getAxiosMessage(err)); console.error("axios failed", err); }
For logging, avoid dumping full Axios error objects without filtering—config may contain headers/tokens.
6) UI formatting: show users what they need, not the entire object
Users should rarely see raw objects or stacks. Prefer:
- A friendly message
- A correlation/request ID
- Optional “details” in developer mode
Example pattern:
tstype UiError = { title: string; message: string; requestId?: string; }; function toUiError(err: unknown): UiError { if (err instanceof Error) { return { title: "Something went wrong", message: err.message }; } if (typeof err === "string") { return { title: "Something went wrong", message: err }; } // last resort return { title: "Something went wrong", message: "Unexpected error" }; }
In React:
jsxconst uiErr = toUiError(error); return ( <div role="alert"> <h2>{uiErr.title}</h2> <p>{uiErr.message}</p> {uiErr.requestId && <code>Request ID: {uiErr.requestId}</code>} </div> );
7) Better stringification options (and when to use them)
JSON.stringify(value, null, 2)
Use when:
- The object is JSON-safe
- You want stable, readable output
jsJSON.stringify({ a: 1, b: { c: 2 } }, null, 2)
Problems:
- Crashes on circular refs
- Poor
Errorserialization
structuredClone (not a stringifier)
structuredClone can help detect non-cloneable values, but it doesn’t produce strings. Not a direct fix, but useful in debugging data shapes.
Safe JSON stringifiers
- fast-safe-stringify
- safe-stable-stringify
They avoid circular reference crashes by replacing cycles.
jsimport safeStringify from "fast-safe-stringify"; console.log(safeStringify(obj));
Node’s util.inspect
Best for debugging complex objects (including Errors) in Node.
jsimport { inspect } from "node:util"; console.log(inspect(err, { depth: null }));
Error serialization
If you need JSON logs with stack traces, ensure your logger supports it or convert explicitly:
jsfunction serializeError(err) { if (!(err instanceof Error)) return err; return { name: err.name, message: err.message, stack: err.stack, cause: err.cause, }; }
8) Preventing [object Object] with TypeScript and linting
The best fix is making it hard to write code that implicitly stringifies objects.
TypeScript: unknown in catch blocks
Enable:
"useUnknownInCatchVariables": true
Then:
tstry { // ... } catch (err) { // err is unknown console.error(String(err)); // explicit }
This forces you to narrow before using err.message.
ESLint rules
Useful rules/patterns:
- Ban string concatenation with non-strings in critical layers (custom rule)
- Encourage template literals? (But template literals still coerce—so use with care.)
- Enforce structured logging API usage
A simple team convention that eliminates most [object Object] logs:
- Never do
"..." + objin logs - Always pass objects as separate arguments (console) or fields (structured logger)
Runtime schema validation (Zod / Joi)
Often the object is coming from outside (API, storage, queue). Validate and normalize early.
tsimport { z } from "zod"; const ErrorPayload = z.object({ message: z.string().optional(), code: z.union([z.string(), z.number()]).optional(), }); function normalizeRemoteError(input: unknown) { const parsed = ErrorPayload.safeParse(input); if (parsed.success) { return parsed.data.message ?? "Remote error"; } return "Remote error"; }
9) A practical checklist for fixing [object Object] fast
- Find the display/log site where
[object Object]appears. - Locate the string conversion:
+ obj,${obj},.textContent = obj, template rendering
- Inspect the underlying value:
typeof,instanceof Error,console.log(obj)as separate arg
- Decide intent:
- For UI: display
messageor a friendly fallback - For logs: keep structured fields
- For UI: display
- Fix upstream error handling:
- throw
Errorinstances, not plain objects - normalize external errors at boundaries
- throw
- Harden with tooling:
- TS
useUnknownInCatchVariables - structured logger conventions
- safe stringification helpers
- TS
10) Patterns you can standardize in a codebase
Pattern: a single formatUnknownError helper
tsexport function formatUnknownError(err: unknown): string { if (err instanceof Error) return err.message; if (typeof err === "string") return err; if (err && typeof err === "object") { // common shapes const anyErr = err as any; if (typeof anyErr.message === "string") return anyErr.message; try { return JSON.stringify(err); } catch { return "[Unstringifiable object]"; } } return String(err); }
Use it in UI layers:
tstoast.error(formatUnknownError(err));
Pattern: always log err as a field
jslogger.error({ err, context: { route, userId } }, "Operation failed");
Pattern: never throw plain objects
If you need metadata, attach it:
tsthrow Object.assign(new Error("Invalid token"), { code: 401 });
Or define an error subclass.
11) Tool comparisons: choosing a logging approach that avoids coercion
console.*
- Pros: zero setup; good object inspection in browser
- Cons: inconsistent formatting across environments; not structured by default
- Best for: local development
Pino
- Pros: high-performance JSON logging; strong ecosystem; good error serializers
- Cons: needs log aggregation formatting downstream
- Best for: Node services where structured logs matter
Winston
- Pros: flexible transports; mature
- Cons: configuration complexity; performance overhead compared to Pino
- Best for: apps needing multiple transport outputs (files, HTTP, etc.)
Sentry (error monitoring)
- Pros: excellent for exceptions; captures stack traces and context
- Cons: not a general-purpose logger; needs careful PII handling
- Best for: production exception monitoring
Regardless of tool: never pre-stringify objects unless you explicitly want a string.
12) Best practices recap
- Treat
[object Object]as a symptom of unintentional coercion. - Keep errors and payloads structured as long as possible.
- For UI: display a specific message field (with a fallback).
- For logs: use structured logging and pass objects as fields.
- Throw Error instances, not plain objects.
- Use TypeScript strictness and catch-
unknownto prevent sloppy handling. - Use safe stringifiers or
util.inspectfor debugging complex objects.
If you share the actual code snippet or stack trace where [object Object] is shown
I can point out the exact coercion site and propose a minimal patch (often it’s one changed line: removing + err and logging err separately, or replacing {error} with {error.message}).
![Troubleshooting “[object Object]”: A Developer’s Guide to Identifying, Preventing, and Fixing Object-to-String Bugs in JavaScript](https://strapi-db-ue2.s3.us-east-2.amazonaws.com/data_bddb61b124.png)