Developers run into "[object Object]" surprisingly often: a log line, a UI label, an API error message, or a templated string suddenly becomes useless. It’s a signal that an object has been coerced into a string—usually unintentionally—and JavaScript is falling back to its default object string representation.
This article explains:
- What
"[object Object]"actually means (and why you see it) - How to inspect objects correctly in Node.js and browsers
- Safe serialization strategies (including circular references)
- Logging best practices for production systems
- Tooling comparisons and debugging workflows
The goal: turn "[object Object]" into structured, actionable output.
1) What "[object Object]" means
"[object Object]" is the result of converting a plain object to a string using the default Object.prototype.toString behavior (directly or indirectly).
The coercion path
When JavaScript needs a string (for concatenation, template interpolation in some contexts, DOM APIs, etc.), it will try to convert the value. For plain objects, that typically becomes:
jsString({ a: 1 }) // "[object Object]" {} + "" // "[object Object]" (or sometimes "0" in odd cases; see below)
Under the hood, it’s essentially:
js({}).toString() // "[object Object]"
For many built-ins, the default “tag” changes:
jsObject.prototype.toString.call([]) // "[object Array]" Object.prototype.toString.call(new Date()) // "[object Date]" Object.prototype.toString.call(null) // "[object Null]" Object.prototype.toString.call(undefined) // "[object Undefined]"
So "[object Object]" isn’t an error by itself. It’s a symptom: something expected a string and got an object.
Common places it happens
- String concatenation
jsconst user = { id: 123, name: "Ada" }; console.log("User: " + user); // User: [object Object]
- Template literals when you embed objects
jsconsole.log(`User: ${user}`); // User: [object Object]
- DOM rendering / UI frameworks
jsconst el = document.querySelector("#output"); el.textContent = user; // [object Object]
- Error messages built from objects
jsthrow new Error("Request failed: " + response);
- URL query strings
jsconst url = "/api?filter=" + { status: "open" }; // /api?filter=[object Object]
2) The right mental model: “inspection” vs “serialization”
A lot of confusion comes from mixing two goals:
- Inspection (debugging): You want a human-friendly view in logs/devtools.
- Serialization (transport/storage): You want a stable string/binary representation for APIs, caches, DBs, messaging.
They overlap, but the best tools differ.
- For inspection:
console.log(obj), Node’sutil.inspect, browser devtools object inspector. - For serialization:
JSON.stringify(with caveats), structured logging libraries, custom serializers.
3) Correct ways to log objects (Node.js and browsers)
3.1 Prefer structured logging over concatenation
Instead of:
jslogger.info("user=" + user);
Do:
jslogger.info({ user }, "Loaded user");
Many loggers (Pino, Bunyan, Winston with JSON formats) will output proper JSON.
If you’re using plain console:
jsconsole.log("Loaded user", user);
This prints the object separately and avoids string coercion.
3.2 console.log vs console.dir
In Node.js:
console.log(obj)uses util inspection.console.dir(obj, options)gives control over depth/colors.
jsconsole.dir(user, { depth: null, colors: true });
In browsers, console.dir can show the object as a navigable tree rather than as a stringified preview.
3.3 Node.js: util.inspect for deterministic object printing
When you need a string (e.g., to embed in an error message) use util.inspect:
jsimport util from "node:util"; const msg = "User payload: " + util.inspect(user, { depth: null, colors: false }); console.error(msg);
Useful options:
depth: how deep to recurse (nullfor unlimited)colors: include ANSI colors (great for terminals, bad for JSON logs)maxArrayLength: avoid massive dumpsbreakLength: line wrapping
3.4 Browser devtools tip: log snapshots, not live references
In Chrome and some browsers, the console may display a live view of an object that can change after logging.
If you want a snapshot, you can log a cloned plain object:
jsconsole.log(JSON.parse(JSON.stringify(obj)));
This is imperfect (drops functions, undefined, symbols, circular refs), but it’s a quick “snapshot” trick.
Better snapshot options in modern environments include structuredClone:
jsconsole.log(structuredClone(obj));
(Still not serializable to JSON, but good for capturing state.)
4) Proper serialization: JSON.stringify (and its traps)
4.1 Basic usage
jsJSON.stringify({ a: 1, b: [2, 3] }); // '{"a":1,"b":[2,3]}' JSON.stringify({ a: 1 }, null, 2); // pretty printed with 2 spaces
So if you really want a string representation:
jsconsole.log(`User: ${JSON.stringify(user)}`);
4.2 Things JSON can’t represent
JSON.stringify will:
- Drop
undefinedvalues in objects - Convert
undefinedin arrays tonull - Drop functions and symbols
- Convert
Dateto ISO string viatoJSON - Fail on circular references
Example:
jsJSON.stringify({ x: undefined, fn() {} }); // '{}' (both dropped) JSON.stringify([undefined]); // '[null]'
4.3 Circular references and how to handle them
A classic source of “it crashed when I tried to stringify it”:
jsconst a = {}; a.self = a; JSON.stringify(a); // TypeError: Converting circular structure to JSON
Option A: Use a “safe stringify” implementation
A common approach is tracking visited objects with a WeakSet:
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 ); } const a = { name: "root" }; a.self = a; console.log(safeStringify(a));
This is good for logs and debugging. It’s not always ideal for data transport because you’re inventing a string placeholder.
Option B: Use libraries designed for this
- fast-safe-stringify: optimized safe JSON stringify for Node
- flatted: encodes circular references in a reversible way
Reversible approaches are useful if you need to deserialize later, but they produce non-standard JSON.
Option C: Avoid serializing huge/cyclic graphs in the first place
Often the best fix is upstream: serialize a DTO (Data Transfer Object) that contains only what you need.
Example: instead of logging an entire request object (which may contain circular references), pick safe fields:
jslogger.info({ method: req.method, url: req.url, requestId: req.id, userId: req.user?.id, }, "Incoming request");
5) Fixing the root cause: where objects get coerced into strings
5.1 String concatenation and template literals
Bad:
js"payload=" + payload
Better:
js"payload=" + JSON.stringify(payload)
Best (structured logging):
jslogger.debug({ payload }, "Sending payload");
5.2 Accidentally binding objects to UI text
In raw DOM:
jsel.textContent = someObject; // coerces to string
Fix:
jsel.textContent = JSON.stringify(someObject, null, 2);
In React-like frameworks, rendering an object directly usually errors or results in a string coercion depending on context.
React example:
jsx// problematic return <div>{user}</div>; // better return <pre>{JSON.stringify(user, null, 2)}</pre>; // best for production UI: render fields explicitly return <div>{user.name}</div>;
5.3 Query strings and URL building
Bad:
jsconst url = `/api/search?filter=${filter}`;
Better:
jsconst url = `/api/search?filter=${encodeURIComponent(JSON.stringify(filter))}`;
Or use URLSearchParams:
jsconst params = new URLSearchParams(); params.set("filter", JSON.stringify(filter)); const url = `/api/search?${params.toString()}`;
5.4 FormData and multipart fields
FormData fields are strings or blobs. Passing an object will coerce:
jsformData.set("meta", { a: 1 }); // becomes "[object Object]"
Fix:
jsformData.set("meta", JSON.stringify({ a: 1 }));
6) Debugging workflow: how to track down where "[object Object]" comes from
6.1 Search patterns
In a codebase, search for:
+ someVarnear logs- Template literals:
`${something}` - DOM assignments:
.textContent =,.innerHTML =(careful with security) - Logger calls:
logger.*("..." + var)
6.2 Break on the coercion
If you can reproduce in browser devtools:
- Open Sources
- Use XHR/fetch breakpoints or DOM breakpoints depending on where it happens
- Add a breakpoint near the rendering/logging
In Node.js:
- Run with
node --inspectand attach Chrome DevTools - Or use
--trace-warnings/--trace-uncaughtfor related errors
6.3 Instrument suspicious values
When you see "[object Object]" in output, log both the value and its type:
jsconsole.log({ value: suspect, type: typeof suspect, tag: Object.prototype.toString.call(suspect), });
This helps distinguish plain objects from e.g. Response, Error, class instances.
6.4 Validate assumptions with runtime checks
For critical boundaries (API layer, logging layer), add guards:
jsfunction assertString(x, name = "value") { if (typeof x !== "string") { throw new TypeError(`${name} must be a string, got ${Object.prototype.toString.call(x)}`); } }
Or for TS projects, validate external inputs with Zod/Valibot.
7) Tooling and library comparisons for logging
7.1 console.*
Pros:
- Built-in, zero config
- Fine for local debugging
Cons:
- Not structured by default
- Harder to query in log aggregation
- Inconsistent formatting across environments
7.2 Pino (Node.js)
Pros:
- Very fast JSON logging
- Great ecosystem (transports, pretty printing)
- Encourages structured logs (
logger.info({ctx}, msg))
Cons:
- Requires setup for prettiness in dev (
pino-pretty)
Example:
jsimport pino from "pino"; const logger = pino({ level: process.env.LOG_LEVEL ?? "info" }); logger.info({ userId: 123 }, "User logged in");
7.3 Winston
Pros:
- Flexible transports and formats
- Common in older codebases
Cons:
- Easier to fall back into string formatting
- Performance can be lower depending on configuration
7.4 Browser logging
In web apps, consider sending structured logs/errors to:
- Sentry (errors + breadcrumbs)
- Datadog RUM
- OpenTelemetry + collector
The principle stays the same: avoid stringifying massive objects; send small structured context.
8) Best practices: logging and serialization in production
8.1 Prefer structured logs
Instead of giant message strings:
jslogger.error(`Payment failed for user ${user}: ${err}`);
Use:
jslogger.error( { userId: user.id, err }, "Payment failed" );
Many loggers have special handling for err (stack traces, message fields).
8.2 Control log volume and sensitive data
Objects often contain secrets:
- passwords
- access tokens
- cookies
- Authorization headers
- PII
Redact aggressively.
Pino example:
jsconst logger = pino({ redact: { paths: [ "req.headers.authorization", "req.headers.cookie", "user.password", "token", ], remove: true, }, });
8.3 Use DTOs at boundaries
For API responses, don’t serialize your domain objects directly (which may include methods, complex graphs, private fields). Map them:
jsfunction userToDTO(user) { return { id: user.id, name: user.name, createdAt: user.createdAt.toISOString(), }; } res.json(userToDTO(user));
This avoids accidental leakage and reduces circular/serialization surprises.
8.4 Don’t rely on .toString() for domain objects
You can customize how objects stringify by implementing:
toString()toJSON()Symbol.toPrimitive
Example:
jsclass User { constructor(id, name) { this.id = id; this.name = name; } toString() { return `User(${this.id}, ${this.name})`; } toJSON() { return { id: this.id, name: this.name }; } } const u = new User(1, "Ada"); String(u); // "User(1, Ada)" JSON.stringify(u); // '{"id":1,"name":"Ada"}'
This can be useful, but use it intentionally—especially toJSON(), which affects any JSON serialization.
8.5 Use OpenTelemetry for cross-service visibility
For distributed systems, logs alone aren’t enough. Combine:
- traces (spans)
- metrics
- logs
Then attach identifiers (traceId, requestId) to logs.
Even if a message has "[object Object]", good correlation can still help you find the actual data elsewhere.
9) Practical examples and fixes
Example A: Express error handler logging request
Problem:
jsapp.use((err, req, res, next) => { console.error("Error for request: " + req); res.status(500).send("oops"); });
req is a massive object with circular refs and you’ll get useless output.
Fix:
jsimport util from "node:util"; app.use((err, req, res, next) => { console.error("Error:", err); console.error("Request meta:", { method: req.method, url: req.originalUrl, ip: req.ip, requestId: req.id, }); // If you really need to inspect headers safely: console.error("Headers:", util.inspect(req.headers, { depth: 2 })); res.status(500).json({ error: "Internal Server Error" }); });
Example B: Fetch wrapper building a query
Problem:
jsconst filter = { status: "open", assignee: "me" }; const res = await fetch(`/api/issues?filter=${filter}`);
Fix:
jsconst params = new URLSearchParams({ filter: JSON.stringify(filter), }); const res = await fetch(`/api/issues?${params}`);
On the server, parse it:
jsconst filter = JSON.parse(req.query.filter);
Example C: Logging errors that contain nested causes
Modern JS supports error causes:
jstry { throw new Error("DB query failed", { cause: new Error("timeout") }); } catch (err) { console.error(err); }
If you stringify errors with JSON.stringify(err), you’ll often lose important fields because Error properties are non-enumerable by default.
Better approaches:
- Log the error object directly (
console.error(err)) - Use a logger that serializes errors properly
- Or convert explicitly:
jsfunction errorToJSON(err) { return { name: err.name, message: err.message, stack: err.stack, cause: err.cause ? errorToJSON(err.cause) : undefined, }; } console.log(JSON.stringify(errorToJSON(err), null, 2));
10) Checklist: avoid "[object Object]" permanently
- Never build log messages by concatenating objects.
- Use
console.log("msg", obj)or structured logging.
- Use
- For “I need a string”, use:
JSON.stringify(obj)for JSON-safe plain datautil.inspect(obj)for debug-friendly strings
- Be cautious with:
FormData.set(name, obj)→ stringify first- query strings → use
URLSearchParams+ JSON - UI
.textContentassignments → stringify or render fields
- Handle circular references:
- avoid logging giant graphs
- use safe stringify when necessary
- Redact secrets and PII.
- Prefer DTO mapping at boundaries.
Closing thoughts
"[object Object]" isn’t a mystery—it's JavaScript telling you: “I turned your object into a string the only way I know how.” The fix is rarely to fight that conversion; it’s to be explicit about your intent.
- If you’re debugging, inspect objects using devtools,
console.dir, orutil.inspect. - If you’re serializing, use
JSON.stringifycarefully, mind circular refs, and map to DTOs. - If you’re operating production systems, adopt structured logging, redaction, and correlation IDs.
Do those consistently, and "[object Object]" becomes an early warning sign you’ll eliminate rather than a recurring annoyance.
![From “[object Object]” to Insight: Debugging, Logging, and Serializing JavaScript Objects Correctly](https://trouvai-blog-media.s3.us-east-2.amazonaws.com/data_b3e5e25b1d.png)