skip to content

Why do developers write Object.prototype.toString.call(value) instead of value.toString() to identify a value in JavaScript, and what does it return for null, [] and new Date()?

level: middleimportance: should knowfreq 40%

answer

  1. nearly every type overrides its own version
  2. borrow the one at the bottom of the chain
  3. the result is a bracketed brand string
  4. reads internal state, not the chain
  5. a well-known symbol can override the tag

basics

~10 s

Object.prototype.toString.call(value) returns a brand string — "[object Null]", "[object Array]", "[object Date]" — read from the value's internal representation. Calling value.toString() instead runs each type's overridden version, and throws outright on null and undefined.

solid answer

~40 s

Almost every built-in overrides `toString`: an array joins its elements, a date formats itself, a number renders digits, and `null` and `undefined` have no method at all so a direct call throws a TypeError. `Object.prototype.toString` is the one un-overridden version, and `.call(value)` borrows it so it runs against any value. It reports a brand read from the value's internal representation rather than its prototype chain, giving `"[object Null]"`, `"[object Array]"` and `"[object Date]"` for those three. Because the brand lives in the value, the answer is identical across realms, which is what makes the idiom useful where `instanceof` fails. Two caveats: since ES2015 any object can override the tag by defining `Symbol.toStringTag`, so the result is spoofable, and it cannot distinguish a subclass from its base or a class instance from a plain object.

code

javascript · 8 lines
javascript
const tag = (v) => Object.prototype.toString.call(v);

console.log(tag(null));       // "[object Null]"
console.log(tag(undefined));  // "[object Undefined]"
console.log(tag([]));         // "[object Array]"
console.log(tag(new Date())); // "[object Date]"
console.log(tag(new Map()));  // "[object Map]"
console.log(tag({ [Symbol.toStringTag]: "Array" })); // "[object Array]"

go deeper

for a junior

Recognise the idiom when you meet it in library code: it produces strings like "[object Array]" and it is safe to call on null and undefined, unlike value.toString(). Reach for Array.isArray first when the question is just "is this an array".

for a middle

Explain the borrowing: nearly every prototype shadows toString, so .call reaches the base implementation, which reports a tag derived from internal slots rather than from the prototype chain. Note that Symbol.toStringTag can replace that tag.

for a senior

Show judgment about trust. Treat the tag as a diagnostic, not a guarantee, and describe the unforgeable alternative — borrowing a slot-backed method and catching the TypeError — for input you did not create, plus the plain-object check libraries actually ship.

for a principal

Argue the policy: when a codebase should standardise one brand-reading helper versus adopting a schema-validation boundary, and what it costs in performance, debuggability and API surface to make runtime type identity a first-class concern across many packages.

## The two toStrings Writing `value.toString()` starts a prototype-chain lookup and almost always finds an override: ```js [1, 2].toString(); // "1,2" — Array.prototype.toString new Date(0).toString(); // a date string — Date.prototype.toString (42).toString(); // "42" — Number.prototype.toString null.toString(); // TypeError — no method to find ``` So the plain call tells you what the value *looks like*, not what it *is*. `Object.prototype.toString` sits at the bottom of the chain and is the only version that reports a type. Since virtually everything shadows it, you have to borrow it explicitly with `.call(value)` — that is all the odd-looking incantation is doing: invoking a specific function with `value` as its `this`. ## What the algorithm does Since ES5, `Object.prototype.toString` follows this order: 1. If `this` is `undefined`, return `"[object Undefined]"`. 2. If `this` is `null`, return `"[object Null]"`. (In ES3 these two cases coerced to the global object instead — a fixed historic wart.) 3. Otherwise convert `this` to an object and pick a builtin tag by inspecting internal state: an array exotic object gives `"Array"`, a callable gives `"Function"`, and objects carrying the relevant internal slot give `"Error"`, `"Boolean"`, `"Number"`, `"String"`, `"Date"`, `"RegExp"`, or `"Arguments"`. Anything else gives `"Object"`. 4. Read the property keyed by `Symbol.toStringTag`. If it is a string, it *replaces* the tag from step 3. 5. Return `"[object " + tag + "]"`. Step 3 is why the idiom is cross-realm safe: it reads the value's own internal representation, not any realm's prototype. An array made in an iframe still tags as `"Array"`, where `instanceof Array` would be false. Step 4 is why newer built-ins appear at all. `Map`, `Set`, `Promise`, `WeakMap`, generator objects and the `Math` and `JSON` namespaces have no dedicated builtin tag — they each define `Symbol.toStringTag` on their prototype, which is how you get `"[object Map]"`. ```js const tag = (v) => Object.prototype.toString.call(v); tag(null); // "[object Null]" tag(undefined); // "[object Undefined]" tag([]); // "[object Array]" tag(new Date()); // "[object Date]" tag(/x/); // "[object RegExp]" tag(() => {}); // "[object Function]" tag(new Map()); // "[object Map]" tag(Math); // "[object Math]" tag(Object.create(null)); // "[object Object]" ``` Note the last one: a prototype-less dictionary object still reports `"[object Object]"`, because the algorithm never consults the chain of the *argument* — only the function you borrowed matters. ## Where it is the right tool - Telling built-in kinds apart when `typeof` collapses them all to `"object"`. - Testing values that may have come from another realm, where prototype identity is meaningless. - Answering "is this a plain object?" in library code — the usual formulation combines a `"[object Object]"` tag with a prototype check for `Object.prototype` or `null`, because the tag alone also matches class instances. ## Where it is the wrong tool **It is spoofable.** Anything can claim any tag: ```js Object.prototype.toString.call({ [Symbol.toStringTag]: "Array" }); // "[object Array]" ``` So it is a debugging and dispatch aid, never a security boundary. Against genuinely hostile input the unforgeable move is to invoke an internal-slot-backed operation through a borrowed method and catch the failure — for example calling `Date.prototype.getTime` with the value as `this` and treating a TypeError as "not a Date". **It is coarse.** Every subclass of `Error` tags as `"Error"`; every class instance you wrote tags as `"Object"`. It answers "which built-in kind", not "which of my types". **It is slower and noisier** than a dedicated predicate. Where a specific check exists — `Array.isArray` above all — use it: it is faster to read and impossible to spoof with a string tag. The useful mental model is a three-tier ladder: `typeof` for the primitive-vs-object-vs-callable split, a dedicated predicate where one exists, and `Object.prototype.toString.call` as the general brand reader for everything in between.

  • Why does Object.prototype.toString.call(new Map()) manage to say "[object Map]" when Map is not in the specification's builtin-tag list?
    Because `Map.prototype` defines a `Symbol.toStringTag` property whose value is the string `"Map"`, and the algorithm's final step lets that property replace the computed tag. Every collection and promise type added since ES2015 works this way rather than getting a hardcoded tag, which is also exactly why a user object can claim any tag it likes.
  • If the tag can be spoofed, how would you check for a real Date against untrusted input?
    Call a method that needs the internal slot and see whether it survives: `Date.prototype.getTime.call(value)` inside a try/catch throws a TypeError for anything that is not a genuine Date, and a forged string tag cannot fake the slot. It is the same trick for other built-ins — borrow an unforgeable accessor, catch the failure.
  • How do you tell a plain object from an instance of a class, given both tag as "[object Object]"?
    Add a prototype test to the tag test: read `Object.getPrototypeOf(value)` and accept only `Object.prototype` or `null`. A class instance's prototype is the class's own prototype object, so it fails. Be aware that the `Object.prototype` comparison is again realm-sensitive, so cross-realm code usually accepts any prototype whose own prototype is null.

saying these in an interview costs you the question

  • Thinks it calls the value's own toString method
  • Says it walks the prototype chain for the constructor name
  • Claims the returned tag cannot be forged
  • Expects a custom class to report its own class name
  • Uses it for arrays where Array.isArray exists

context