JavaScript [object Object]: Meaning and Fixes
The profile card was supposed to say Customer: Maya. It said Customer: [object Object], because the code joined a label to the whole customer object instead of the name property inside it.
[object Object]is JavaScript’s default string representation for an ordinary object, produced when the object is converted to text without a custom primitive value.
What Does [object Object] Mean?
A JavaScript object is a value containing properties, where each property associates a key with another value. This object has two:
const customer = {
name: 'Maya',
plan: 'team',
};
console.log(customer.toString());
console.log(String(customer));
[object Object]
[object Object]
The exact text is [object Object]. There is no comma, and the second Object starts with a capital letter. [object, object], [Object object] and [object object] are incorrect spellings.
The first object is the fixed prefix used by Object.prototype.toString(). The second component is the built-in tag or a tag supplied by Symbol.toStringTag. That inherited method constructs a string from [object , the tag and ], as defined by the ECMAScript specification.
The string does not contain the object’s properties. Nothing about [object Object] reveals that this customer is named Maya or uses the team plan. It reports a default representation, not a readable dump of the value.
It is usually not an error either. JavaScript completed the requested conversion and returned a string. The bug, when there is one, is asking it to convert the whole object when the interface needed customer.name.
Object to primitive conversion covers the wider conversion rules. Here, we’ll follow the exact path that produces this particular result and choose a fix based on what the text is for.
Object, object, and [object Object] Are Different
The same word appears in three forms, and each names a different thing.
Objectis the global constructor and function. It also owns methods such asObject.keys(),Object.entries()andObject.create().objectis the value returned bytypeoffor ordinary objects and many other non-primitive values.[object Object]is a string that conversion can produce. Itstypeofresult isstring.
Here are all three in one example:
const customer = Object({
name: 'Maya',
});
const label = String(customer);
console.log(typeof Object);
console.log(typeof customer);
console.log(typeof label);
console.log(label);
function
object
string
[object Object]
Object creates or returns an object value here. typeof customer describes that value as an object. String(customer) then produces a separate string containing [object Object].
That distinction matters when you search for a fix. If the problem concerns properties, prototypes or methods, you are working with an object value. If the problem is unwanted text in a page, URL or message, you are looking at the result of conversion.
Objects store keyed properties, but those properties do not automatically become display text. Key-Value Pairs in JavaScript: Objects vs Maps covers how those entries differ from Map entries, while Object methods, “this” explains the methods that objects inherit or define.
How JavaScript Produces [object Object]
JavaScript cannot place an object directly inside a string. It first runs an abstract operation called ToPrimitive, which asks the object for a primitive value such as a string or number.
For conversion with the string hint, the order is fixed:
- Look for a method stored under
Symbol.toPrimitive. - If that method is absent, call
toString(). - If
toString()returns another object, callvalueOf(). - If no step produces a primitive, throw a
TypeError.
If Symbol.toPrimitive supplies a callable method, JavaScript calls it first with the hint "string". A non-nullish, non-callable value throws a TypeError. Returning an object from a callable hook also throws a TypeError; JavaScript does not continue to toString().
A plain object with Object.prototype in its prototype chain and no conversion overrides has no Symbol.toPrimitive method. Its inherited toString() is Object.prototype.toString(), which returns [object Object]. That is a primitive string, so conversion stops before valueOf().
The following browser-console tracer lets you select each branch. It mirrors the string-hint order so the calls stay visible; change the prompt selection to compare a plain object, a custom hook, a non-callable hook, a Symbol-returning hook, an overridden method, a fallback to valueOf() and a null-prototype object:
const cases = {
ordinary: {
name: 'Maya',
},
nonCallableHook: {
name: 'Maya',
[Symbol.toPrimitive]: true,
},
symbolHook: {
name: 'Maya',
[Symbol.toPrimitive]() {
return Symbol('token');
},
},
primitiveHook: {
name: 'Maya',
[Symbol.toPrimitive](hint) {
return `${hint}:Maya`;
},
},
customToString: {
name: 'Maya',
toString() {
return 'Customer Maya';
},
},
valueOfFallback: {
toString() {
return { not: 'primitive' };
},
valueOf() {
return 7;
},
},
bare: Object.assign(Object.create(null), {
name: 'Maya',
}),
};
function isPrimitive(value) {
return value === null ||
(typeof value !== 'object' && typeof value !== 'function');
}
function toStringPrimitive(value) {
if (typeof value === 'symbol') {
throw new TypeError('Cannot convert a Symbol value to a string');
}
return String(value);
}
function traceStringConversion(input) {
console.log('1. check Symbol.toPrimitive');
const primitiveHook = input[Symbol.toPrimitive];
if (primitiveHook !== null && primitiveHook !== undefined) {
if (typeof primitiveHook !== 'function') {
throw new TypeError('Symbol.toPrimitive is not callable');
}
console.log('2. call Symbol.toPrimitive("string")');
const result = primitiveHook.call(input, 'string');
if (!isPrimitive(result)) {
throw new TypeError('Symbol.toPrimitive returned an object');
}
return toStringPrimitive(result);
}
console.log('2. no Symbol.toPrimitive');
for (const methodName of ['toString', 'valueOf']) {
const method = input[methodName];
if (typeof method !== 'function') {
console.log(`3. ${methodName} is missing`);
continue;
}
console.log(`3. call ${methodName}()`);
const result = method.call(input);
if (isPrimitive(result)) {
return toStringPrimitive(result);
}
console.log(`4. ${methodName} returned an object`);
}
throw new TypeError('No primitive value was produced');
}
const selected =
prompt(
'Choose ordinary, nonCallableHook, symbolHook, primitiveHook, customToString, valueOfFallback, or bare',
'ordinary'
) || 'ordinary';
const input = cases[selected];
for (const [label, convert] of [
['tracer', traceStringConversion],
['String', String],
]) {
try {
console.log(`${label} result:`, convert(input));
} catch (error) {
console.log(`${label} error:`, error.name);
}
}
The ordinary path stops at inherited toString() and returns [object Object]. customToString returns Customer Maya, while valueOfFallback reaches the second ordinary method because its toString() returned an object.
The bare case is different. Object.create(null) creates an object with no prototype, so it inherits neither toString() nor valueOf(). String conversion cannot find a primitive and throws:
const settings = Object.create(null);
settings.theme = 'dark';
try {
console.log(String(settings));
} catch (error) {
console.log(error.name);
}
TypeError
The prototype is what supplies the ordinary conversion methods. Prototypal inheritance follows that lookup chain in full.
One wrinkle belongs to the + operator. Explicit string conversion and template interpolation use the string hint, but + starts with the default hint. For a plain object with Object.prototype in its prototype chain and no conversion overrides, that path finds no Symbol.toPrimitive hook, then tries valueOf() before toString(); inherited valueOf() returns the object, so conversion continues and still reaches [object Object]. The addition operator rules define that separate path.
Where [object Object] Appears
The unwanted string appears wherever an API or expression needs text and receives an ordinary object.
Start with four direct conversions:
const customer = {
name: 'Maya',
plan: 'team',
};
console.log(customer.toString());
console.log(String(customer));
console.log(`Customer: ${customer}`);
console.log('Customer: ' + customer);
[object Object]
[object Object]
Customer: [object Object]
Customer: [object Object]
The explicit toString() call invokes the inherited method directly. String(customer) requests string conversion, and the template literal converts the value inserted at ${customer}. Concatenation converts both operands to primitives before joining them.
alert() produces the same visible text in a browser. This is a conversion demonstration, not a recommended interface for presenting application data:
const customer = {
name: 'Maya',
};
alert(customer); // displays [object Object]
DOM text has the same problem when the whole object is assigned instead of a property:
const customer = {
name: 'Maya',
};
const status = document.createElement('p');
status.textContent = customer;
document.body.append(status);
// the page shows [object Object]
The element needs text, so the object is converted. The intended assignment names the value a person should see:
status.textContent = `Customer: ${customer.name}`;
Console calls can hide or expose the conversion boundary. These two lines are not equivalent:
const customer = {
name: 'Maya',
plan: 'team',
};
console.log('customer:', customer);
console.log('customer: ' + customer);
The first call passes two arguments. The second argument remains an object for the console to present using its own inspection interface, whose exact appearance depends on the host and developer tools.
The second call passes one argument. Concatenation has already converted the object, so the console receives only customer: [object Object]. It cannot reconstruct the lost properties from that string.
How to Display or Inspect an Object
There is no single replacement for [object Object], because three different jobs lead to it.
| Goal | Use | Why |
|---|---|---|
| Debug inspection | console.log('customer:', customer) | The object remains a separate value for developer tools to inspect |
| User-facing rendering | Select properties such as customer.name | The interface shows the information the person needs |
| Serialization | JSON.stringify(value, null, 2) | The result is JSON text for compatible data |
Inspection, rendering and serialization are different jobs. Picking the goal first prevents one tool from being stretched into all three.
For debugging, pass the object separately:
console.log('customer:', customer);
Developer tools may show expandable properties, but their presentation is host-specific. Do not depend on that presentation as application output.
For a person using the application, choose and format the relevant properties:
const customer = {
name: 'Maya',
plan: 'team',
seats: 6,
};
const message =
`${customer.name} uses the ${customer.plan} plan for ${customer.seats} seats.`;
console.log(message);
Maya uses the team plan for 6 seats.
That sentence has an audience and a purpose. Dumping every property would expose implementation detail and still leave the reader to interpret it.
When a compact property list genuinely fits the interface, Object.entries() gives you key-value pairs:
const customer = {
name: 'Maya',
plan: 'team',
};
const details = Object.entries(customer)
.map(([key, value]) => `${key}=${value}`)
.join(', ');
console.log(details);
name=Maya, plan=team
This works because both values are already suitable for text. A nested object would need its own formatting rule or it would produce [object Object] again.
For transport, storage or a readable JSON panel, JSON.stringify() converts compatible data into JSON text. Its third argument controls indentation:
const customer = {
name: 'Maya',
plan: 'team',
};
console.log(JSON.stringify(customer, null, 2));
{
"name": "Maya",
"plan": "team"
}
That result contains the enumerable own string-keyed data that JSON serialization visits. It is not a complete view of everything associated with the object, and the next section covers the values it changes, skips or rejects.
For a broader foundation covering objects, arrays, functions and conversion together, JavaScript Fundamentals keeps these rules in one course sequence.
When JSON.stringify() Is Not Enough
JSON.stringify() is a serializer, not a universal object inspector. JSON has a smaller set of values than JavaScript, so some information must disappear, change shape or cause the operation to fail.
Here is the full set of common surprises in one standalone example:
const record = {
kept: 1,
missing: undefined,
calculate() {
return 2;
},
token: Symbol('token'),
};
const list = [
1,
undefined,
function calculate() {},
Symbol('token'),
];
const map = new Map([['name', 'Maya']]);
const set = new Set(['draft', 'published']);
console.log(JSON.stringify(record));
console.log(JSON.stringify(list));
console.log(String(JSON.stringify(undefined)));
console.log(JSON.stringify(map), JSON.stringify(set));
try {
JSON.stringify({ total: 19n });
} catch (error) {
console.log(error.name);
}
const circular = { name: 'Maya' };
circular.self = circular;
try {
JSON.stringify(circular);
} catch (error) {
console.log(error.name);
}
const account = {
id: 7,
passwordHash: 'hidden',
toJSON() {
return { id: this.id };
},
};
console.log(JSON.stringify(account));
{"kept":1}
[1,null,null,null]
undefined
{} {}
TypeError
TypeError
{"id":7}
Inside an object, properties holding undefined, functions or symbols are omitted. Inside an array, those values become null, preserving the array positions. Serializing undefined by itself returns undefined rather than JSON text.
A BigInt causes a TypeError unless custom serialization behavior supplies a JSON-compatible value. Map and Set normally become {} because their contents are not enumerable own string-keyed properties.
Circular references fail too. circular.self points back to circular, but JSON has no representation for that object reference, so serialization throws a TypeError.
Finally, toJSON() changes what gets serialized. The account object returns { id: 7 }, so passwordHash never reaches the JSON result. The serializer uses the returned value instead of walking the original object directly.
These behaviors are defined in MDN’s JSON.stringify() reference.
How to Customize Object Conversion Safely
JavaScript has four customization hooks that sound related but do different jobs.
| Hook | Controls |
|---|---|
toString() | Ordinary conversion when JavaScript reaches the object’s string method |
Symbol.toPrimitive | Primitive conversion for the "string", "number" and "default" hints |
Symbol.toStringTag | The tag displayed by Object.prototype.toString() |
toJSON() | The value used by JSON.stringify() |
Override toString() when the object has one compact, stable text label:
const customer = {
name: 'Maya',
toString() {
return `Customer ${this.name}`;
},
};
console.log(String(customer));
console.log(`Selected: ${customer}`);
Customer Maya
Selected: Customer Maya
Use Symbol.toPrimitive when string and numeric contexts need different primitives:
const invoice = {
total: 19,
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return this.total;
}
return `Invoice ${this.total}`;
},
};
console.log(String(invoice));
console.log(+invoice);
console.log(`${invoice}`);
Invoice 19
19
Invoice 19
The hook must return a primitive. Returning an object throws a TypeError immediately. Symbol type covers symbol-keyed hooks and properties beyond conversion.
Symbol.toStringTag changes only the tag chosen by Object.prototype.toString():
const queue = {
[Symbol.toStringTag]: 'JobQueue',
};
console.log(Object.prototype.toString.call(queue));
[object JobQueue]
That property does not replace Symbol.toPrimitive, and it should not be treated as a dependable type check because any object can supply a string-valued tag.
Use toJSON() only when the serialized representation should differ from the live object. It does not control template literals, concatenation or String(value).
Choose the fix by the context:
- For debugging, pass the object as a separate console argument.
- For page text, messages and labels, select and format the properties people need.
- For JSON transport or storage, use
JSON.stringify()and account for its omissions and failures. - For a domain object with a meaningful text form, define
toString()orSymbol.toPrimitive. - For a custom
Object.prototype.toString()tag, defineSymbol.toStringTag. - For a custom JSON shape, define
toJSON().
The unwanted [object Object] disappears once the code names its actual job. Inspection keeps the object, rendering selects its data, and serialization accepts the limits of JSON.