skip to content

String primitives have no properties of their own, so why does `'abc'.length` work, and why does assigning `s.foo = 1` to a string variable never stick?

level: middleimportance: must knowfreq 70%

answer

  1. primitives borrow, they do not own
  2. spec step: ToObject on the base
  3. wrapper made fresh and discarded each access
  4. the write lands on a throwaway
  5. strict mode throws instead of ignoring

basics

~20 s

Property access on a primitive makes the engine wrap it in a temporary String, Number, Boolean, Symbol or BigInt object, read the property from that wrapper, then discard it. Writes land on the throwaway wrapper, so they vanish.

solid answer

~50 s

When you write `'abc'.length`, the property reference forces the engine to convert the primitive to an object — the spec's ToObject step — producing a fresh `String` wrapper whose prototype is `String.prototype`. The property is read from that wrapper, and the wrapper is then unreachable and collected; engines optimize the common cases so no object is really allocated, but the semantics are as if one were. That is why every primitive except `null` and `undefined` appears to have methods: `(5).toFixed(2)`, `true.toString()`, `sym.description` all box first. It also explains the write case. `s.foo = 1` creates the property on a temporary wrapper that is immediately thrown away, so the next `s.foo` boxes a *new* wrapper and reads `undefined`. In sloppy mode that write is silently ignored; in strict mode — which includes all modules and class bodies — it throws a TypeError instead of failing quietly.

code

javascript · 13 lines
javascript
const s = 'abc';

console.log(s.length);        // 3 — read from a temporary String wrapper
console.log(s.toUpperCase()); // 'ABC' — method lives on String.prototype

try {
  s.foo = 1;                  // strict mode (modules, classes): TypeError
} catch (e) {
  console.log(e.constructor.name); // 'TypeError'
}

console.log(s.foo);           // undefined — a new wrapper, with no foo
console.log(typeof s);        // 'string' — s was never converted

go deeper

for a junior

Be able to say that primitives borrow their methods: the engine temporarily wraps the value so the prototype's methods are reachable. Know that you cannot store a property on a string, number, or boolean.

for a middle

Explain the mechanism precisely — property access converts the base to a wrapper object, reads from it, then discards it — and use that to explain why writes vanish and why each access sees a fresh wrapper.

for a senior

Show the failure modes in real code: a silent write in a legacy script that starts throwing once the file becomes a module, and sloppy-mode prototype methods where this arrives boxed rather than as the primitive. Say how you would carry per-value metadata instead.

for a principal

Own the reasoning about why the language kept an object-shaped read path over value types at all, what it buys (one uniform prototype-based method dispatch) and what it costs (a silent-write footgun that only strict mode closes). Be ready to argue for strict-by-default across a codebase on that evidence.

## The apparent contradiction A primitive is a value, not an object: it has no own properties and no place to store any. Yet `'abc'.length` is 3, `'abc'.toUpperCase()` returns `'ABC'`, `(255).toString(16)` returns `'ff'`, and `true.valueOf()` returns `true`. If primitives have nothing on them, where do the property and the method come from? ## Autoboxing: ToObject on property access The answer is that evaluating a property reference whose base is a primitive triggers a conversion. The specification's property-lookup path applies **ToObject** to the base value, which creates a wrapper object of the corresponding type: a `String` object for a string, `Number` for a number, `Boolean` for a boolean, `Symbol` for a symbol, `BigInt` for a bigint. That wrapper holds the primitive in an internal slot, and its prototype is the matching built-in prototype — which is where `toUpperCase`, `toFixed`, `description` and friends actually live. The property is read from the wrapper, and then the wrapper is gone; nothing in the program holds a reference to it. So `'abc'.length` is really: box `'abc'` → read `length` off the temporary `String` object (its own `length` slot, reflecting the UTF-16 code-unit count) → discard. Engines do not literally allocate an object for every character access — this is one of the most heavily optimized paths in any JavaScript engine — but the observable semantics are exactly as described, and the observable part is what interviews probe. ## Why writes disappear Once you see that the wrapper is temporary, the write case follows immediately: ```js let s = 'abc'; s.foo = 1; // creates 'foo' on a throwaway wrapper s.foo; // boxes a BRAND NEW wrapper, which has no 'foo' → undefined ``` Each property access creates its own wrapper. The one that received `foo` was discarded before the second line ran. The variable `s` still holds the primitive `'abc'`; nothing about it changed. This is a favourite interview trap because people expect the assignment either to work or to throw, and in sloppy mode it does neither — it silently evaporates. ## Strict mode makes the failure loud In strict mode the same assignment throws: ```js 'use strict'; let s = 'abc'; s.foo = 1; // TypeError: Cannot create property 'foo' on string 'abc' ``` This matters more than it used to, because ES modules and class bodies are strict mode automatically. Code that silently swallowed the mistake in an old script now throws once it moves into a module. Being able to say *both* behaviours, and name which context you are in, is what separates a complete answer from half of one. ## The `this`-boxing corollary The same conversion applies to the `this` value of a non-strict function. If you add a method to `String.prototype` in a sloppy-mode script and call it on a primitive, `this` inside is the wrapper object, not the string: ```js // in a non-strict script (not a module) String.prototype.kind = function () { return typeof this; }; 'ab'.kind(); // 'object' ``` Add `'use strict'` to the function body — or write the same function inside a module, where strictness is automatic — and `this` stays the primitive, so it returns `'string'`. This bites people who write `this === 'ab'` inside such a method and find it false, or who compare `this` against a string with `switch`. In strict code the primitive is passed through untouched, which is the saner behaviour and the reason the change was made. ## `null` and `undefined` have no wrapper They are the two primitives with no corresponding wrapper type, so ToObject on them fails and property access throws: ```js null.length // TypeError: Cannot read properties of null (reading 'length') undefined.foo // TypeError: Cannot read properties of undefined (reading 'foo') ``` That is the mechanical reason behind the single most common runtime error in JavaScript, and behind the existence of optional chaining as a way to short-circuit before the access happens. ## Practical consequences Three things worth carrying away. First, you cannot tag a primitive with metadata — there is no place to put it; use a `Map` keyed by the value, or carry an object instead. Second, a value's methods coming from a prototype means monkey-patching `String.prototype` affects every string in the realm, and your patch will see a wrapper as `this` unless you write it in strict code. Third, the fact that reads work and writes vanish is asymmetric on purpose: reads have a well-defined result from the prototype chain, while writes have nowhere durable to go.

  • Does the engine really allocate an object every time you read `str.length`?
    Semantically yes, observably no. The specification describes property access on a primitive as converting it to a wrapper object, but engines special-case these paths heavily — reading `length` off a string compiles down to reading the stored length, with no allocation. The abstraction only becomes visible when you do something that captures the wrapper, such as passing a primitive as the `this` of a sloppy-mode function.
  • Why do `null.foo` and `undefined.foo` throw instead of returning undefined?
    Because null and undefined are the two primitives with no wrapper type. Property access first converts the base value to an object, and that conversion has no defined result for them, so it throws a TypeError before any lookup happens. Every other primitive — string, number, boolean, symbol, bigint — has a wrapper to convert into, which is why they all appear to carry methods.
  • You want to attach metadata to a particular string value. What are your options?
    Not on the string itself — there is nowhere for it to live, and in strict mode the attempt throws. Either carry an object that has the string as a field, or keep a side table: a `Map` keyed by the string value, since map keys compare by value for primitives. A `WeakMap` will not work, because it requires object keys and rejects primitives.

saying these in an interview costs you the question

  • Claiming strings are secretly objects in JavaScript
  • Saying the assignment mutated the string
  • Expecting s.foo to read back as 1 later
  • Not knowing strict mode turns the silent write into a TypeError
  • Thinking null and undefined get boxed too

context