JavaScript [object Object]: Meaning and Fixes

Aug 24, 2026·21 min read

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.

The properties do not enter the stringcustomer objectname: Mayaplan: teamconvert[objectObject]The values stay in the object; the result is only a tag.
String conversion reports a generic object tag, not the object’s properties.

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.

  • Object is the global constructor and function. It also owns methods such as Object.keys(), Object.entries() and Object.create().
  • object is the value returned by typeof for ordinary objects and many other non-primitive values.
  • [object Object] is a string that conversion can produce. Its typeof result is string.

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].

Three similar names, three differentthingsObjectglobal constructor · typeof is functioncreates or returnscustomer valuehas properties · typeof is objectString(customer)[object Object]separate value · typeof is string
Object, object, and [object Object] refer to different stages and values.

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:

  1. Look for a method stored under Symbol.toPrimitive.
  2. If that method is absent, call toString().
  3. If toString() returns another object, call valueOf().
  4. 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().

Where string conversion stopsSymbol.toPrimitivecheck the custom hookprimitive?stop hereyeshook absenttoString()ordinary string methodprimitive?stop hereyesreturned objectvalueOf()last ordinary fallbackprimitive?stop hereyesstill an objectthrow TypeErrorAn invalid custom hook also throws.
String-hint conversion stops at the first primitive result.

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.

The conversion boundary changeswhat arrivesSeparate argumentcustomerobjectname + planstill an objectconsoleinspectorproperties expandConcatenation firstcustomername + planconvertcustomer: [objectObject]only this string arrivesOnce converted, the name and plan cannot berecovered.
Separate arguments preserve the object; concatenation converts it first.

How to Display or Inspect an Object

There is no single replacement for [object Object], because three different jobs lead to it.

GoalUseWhy
Debug inspectionconsole.log('customer:', customer)The object remains a separate value for developer tools to inspect
User-facing renderingSelect properties such as customer.nameThe interface shows the information the person needs
SerializationJSON.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.

One object, three different jobscustomer objectname · plan · seatspreserveselectserializeDebuggingkeep the liveobject for theconsole toinspectRenderingchoose whata person needs:Maya · team· 6 seatsJSONmake datacompatible withtransport orstorageThe right output depends on its audience.No single string representation serves all three jobs.
Inspection, rendering, and serialization preserve different parts of an object.

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.

JSON can describe a tree, not a loopJavaScript graphcircularname: Mayaself: referenceself returns to the same objectstringifyJSON treerootMayaloop?throw TypeError
A circular object graph cannot be represented as an ordinary JSON tree.

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.

HookControls
toString()Ordinary conversion when JavaScript reaches the object’s string method
Symbol.toPrimitivePrimitive conversion for the "string", "number" and "default" hints
Symbol.toStringTagThe 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() or Symbol.toPrimitive.
  • For a custom Object.prototype.toString() tag, define Symbol.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.

Frequently asked questions

What does [object Object] mean in JavaScript?
[object Object] is the default string representation of an ordinary JavaScript object. It appears when JavaScript needs a string but receives an object that has no more specific primitive conversion.
Is [object Object] a JavaScript error?
No. It is a string produced during object conversion, although seeing it in a page or message often means the code converted the whole object when it should have selected a property.
How do I display an object instead of [object Object]?
Render the properties a person needs, such as user.name, or format selected entries with Object.entries(). For debugging, pass the object as a separate console argument; for serialization, use JSON.stringify() after checking its limitations.
Why does console.log show an object but string concatenation shows [object Object]?
A separate console argument remains an object for the console to inspect. Concatenation must produce one string first, so JavaScript converts the object before console.log receives it.
Can JSON.stringify fail on an object?
Yes. It throws on BigInt values without custom serialization and on circular references. It also omits or changes some values and normally loses the contents of Map and Set.