skip to content

Strings and Symbols

Strings are immutable UTF-16 sequences with a large method surface, and symbols are the primitive designed to be a collision-free key. They pair naturally because both are about how JavaScript represents identity — in text, and in property names.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

15

In JavaScript, what happens when you assign to a character position of a string, as in `let s = 'hello'; s[0] = 'H';`, and what does that tell you about how string methods behave?

level: juniorimportance: must knowfreq 65%

answer

  1. the value versus the variable
  2. index properties are read-only
  3. primitives get a throwaway wrapper
  4. sloppy ignores, strict throws
  5. methods produce, never edit

basics

~20 s

JavaScript strings are immutable, so the assignment silently does nothing in sloppy mode and throws a TypeError in strict mode; s stays "hello". Every string method, such as toUpperCase or trim, returns a brand-new string instead of editing the original.

solid answer

~40 s

Strings in JavaScript are immutable primitives, so there is no way to edit one in place. Index properties of a string are non-writable, and writing through a primitive first boxes it into a throwaway `String` wrapper, so the write is discarded — silently in sloppy mode, and with a `TypeError` in strict mode (which includes all ES module code and class bodies). The practical consequence is that every method that looks like an edit — `toUpperCase()`, `trim()`, `replace()`, `slice()`, `padStart()` — leaves the receiver untouched and hands back a new string. That is why `input.trim();` on its own line is a classic bug: you have to use or reassign the result. Note also that `s = s.toUpperCase()` is rebinding the variable, not mutating the value.

code

javascript · 15 lines
javascript
let s = 'hello';
s[0] = 'H';
console.log(s); // "hello" — the write was silently discarded

function strictWrite() {
  'use strict';
  const t = 'hello';
  t[0] = 'H';
}

try {
  strictWrite();
} catch (err) {
  console.log(err.constructor.name); // "TypeError"
}

go deeper

for a junior

Be ready to say plainly that strings cannot be changed in place: assigning to an index does nothing useful, and trim, toUpperCase and replace hand back a new string you must assign or use.

for a middle

Explain the mechanism rather than the slogan: index properties on strings are non-writable, and a write through a primitive goes via a temporary wrapper object, so it is discarded in sloppy mode and throws a TypeError in strict mode.

for a senior

Show where the discarded-result bug appears in real code — a lone sanitising call whose value is never used — and how you catch it systematically with a lint rule for unused expressions plus a test at the boundary that was supposed to clean the input.

for a principal

Own the tradeoff: immutable strings remove a whole class of aliasing bugs and make text safe to share as cache and map keys without defensive copies, while incremental string building is an engine optimisation rather than a language guarantee and deserves measurement, not folklore.

## What "immutable" means here A JavaScript string is a primitive value, like a number or a boolean. The character sequence it holds is fixed the moment the value exists, and nothing in the language can change it afterwards. Anything that looks like an edit — uppercasing, trimming, replacing, slicing — actually produces a *different* string value and leaves the original exactly as it was. This is a property of the *value*, not of the *variable*. `let s = 'hello'; s = 'world';` is perfectly legal: you rebound the name `s` to a different string. What you cannot do is turn the string `'hello'` itself into `'Hello'`. ## Why `s[0] = 'H'` does nothing Two separate mechanisms conspire here. First, indexed properties of a string are read-only. In spec terms a String exotic object exposes each index as a property with `writable: false`, `enumerable: true`, `configurable: false`. You can observe this directly: ```javascript console.log(Object.getOwnPropertyDescriptor(Object('hi'), '0')); // { value: 'h', writable: false, enumerable: true, configurable: false } ``` Second, `s` in the example is a *primitive*, not an object, and primitives have no properties of their own. When you write `s[0] = 'H'`, the engine temporarily wraps the primitive in a `String` object, attempts the assignment on that wrapper, and then throws the wrapper away. Even if the property were writable, the mutation would land on an object that is discarded on the next line. The outcome depends on the mode: ```javascript let s = 'hello'; s[0] = 'H'; console.log(s); // "hello" — the write was silently discarded function strictWrite() { 'use strict'; const t = 'hello'; t[0] = 'H'; // TypeError: Cannot assign to read only property '0' } ``` Sloppy mode ignores a failed assignment to a non-writable property; strict mode throws a `TypeError`. Since ES module code and class bodies are strict by default, in a modern codebase this usually throws rather than fails quietly — which is the friendlier outcome. ## Methods return new strings No method on `String.prototype` mutates its receiver. Every one of them is a producer: ```javascript const raw = ' Ada '; raw.trim(); // returns "Ada" — and the result is thrown away console.log(raw); // " Ada " — unchanged const clean = raw.trim(); // keep the result console.log(clean); // "Ada" ``` The forgotten-result bug is by far the most common way this bites people, and it is easy to miss in review because the line reads like it does something. A lint rule against unused expressions catches most instances. Contrast this with arrays, where some methods mutate (`push`, `sort`, `reverse`) and some copy. Strings have no such split: there is nothing to mutate. ## Reassignment is not mutation ```javascript let name = 'ada'; name = name.toUpperCase(); // rebinding the variable console.log(name); // "ADA" const fixed = 'ada'; // fixed = fixed.toUpperCase(); // TypeError: Assignment to constant variable ``` `const` prevents rebinding the *name*. It is not what makes the string immutable — the string was already immutable, and `let` does not make it any less so. Similarly, `Object.freeze('abc')` is pointless: since ES2015 it simply returns the primitive unchanged, because there was never anything mutable to freeze. ## Why this design is convenient Because a string can never change underneath you, passing one to a function is inherently safe: the callee cannot corrupt the caller's value, and you never need a defensive copy. The same property makes strings dependable keys in a `Map` or in a cache, and lets engines share and intern string data behind the scenes. The one cost people worry about is building a long string incrementally. The spec says each `+=` produces a new value, which sounds like quadratic work, but real engines represent concatenations with internal shared structures and flatten them lazily, so ordinary loop concatenation is usually fine. That is an engine optimisation, not a language guarantee — if it matters for your workload, measure it rather than repeating folklore. ## What an interviewer is really checking They want to see that you distinguish *value* from *variable*, that you know method calls are producers, and that you can name the strict-vs-sloppy difference rather than saying vaguely "it doesn't work".

  • Why does the same assignment throw in one file and fail silently in another?
    Because of strict mode. A failed write to a non-writable property is ignored in sloppy mode but raises a `TypeError` in strict mode. ES modules and class bodies are strict automatically, and a classic script becomes strict only with the `'use strict'` directive, so identical code can behave differently depending on where it lives.
  • If strings are immutable, why does `const` still matter for a string variable?
    `const` constrains the binding, not the value: it stops the name being reassigned later. The string itself needed no protection — it was already unchangeable. So `const` here is about preventing accidental rebinding and signalling intent, not about immutability of the text.
  • How would you spot this bug in a code review?
    Look for a method call used as a standalone statement — `input.trim();`, `s.replace(/x/, 'y');` — with the result discarded. Since no string method mutates, such a line is always dead code. A lint rule for unused expressions flags them mechanically, and a test at the boundary that was supposed to sanitise the value catches the rest.

saying these in an interview costs you the question

  • Claims s[0] = 'H' edits the string in place
  • Says string methods modify the receiver like Array.prototype.push
  • Thinks const is what makes a string immutable
  • Believes the assignment always throws, regardless of mode
  • Suggests Object.freeze to make a string immutable

context

open as a page

In JavaScript, what is a Symbol, and what does using a symbol as an object property key give you that a string key does not?

level: juniorimportance: must knowfreq 58%

basics

~20 s

A Symbol is a primitive whose every value is unique: Symbol('id') never equals another Symbol('id'). Used as a property key, it cannot collide with any string key or with symbols created by other code, even when the descriptions match.

open as a page

In JavaScript, what is a template literal (a string written in backticks), and how does it differ from building the same string with the + operator?

level: juniorimportance: must knowfreq 80%

basics

~20 s

A template literal is a backtick-delimited string that embeds any expression with ${...} and keeps newlines exactly as typed. Interpolated values are converted to strings automatically, so it produces what + would for most inputs, with far less quote juggling.

open as a page

Why does `'🙂'.length` evaluate to 2 in JavaScript, and how do you count what a user would call one character?

level: middleimportance: must knowfreq 58%

basics

~20 s

JavaScript's .length counts UTF-16 code units, not characters. The 🙂 emoji sits above the Basic Multilingual Plane, so it is stored as a two-unit surrogate pair. Iterating the string, for example with [...str], walks whole code points and yields 1.

open as a page

When you call a JavaScript tagged template such as tag`a${x}b`, what arguments does the tag function actually receive, and what is the relationship between them?

level: middleimportance: must knowfreq 52%

basics

~20 s

The tag function receives the static text chunks as its first argument — an array with a raw property holding the unescaped text — followed by one argument per interpolated value. The chunks array always has exactly one more element than the number of values.

open as a page

Which JavaScript operations skip symbol-keyed properties on an object, and which ones still expose or copy them?

level: middleimportance: should knowfreq 44%

basics

~10 s

Symbol keys are skipped by for...in, Object.keys/values/entries, Object.getOwnPropertyNames and JSON.stringify. They are still returned by Object.getOwnPropertySymbols and Reflect.ownKeys, and copied by Object.assign and object spread. Hidden from loops, not private.

open as a page

Why does `const s = Symbol('id'); console.log('id: ' + s);` throw in JavaScript, while `String(s)` returns a string?

level: middleimportance: should knowfreq 38%

basics

~20 s

Implicit coercion of a symbol to a string throws a TypeError by design, so a symbol can never silently become a string property key. String(s) and s.toString() are explicit requests, and both return the string 'Symbol(id)'.

open as a page

Truncating user-supplied text keeps producing broken emoji and stray replacement characters in production. Working in JavaScript, how do you cut a string safely, and what unit should you cut on?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Cut on grapheme clusters, not code units or code points. Use Intl.Segmenter with granularity 'grapheme' to walk user-perceived characters and join the first N. Slicing by index splits surrogate pairs, and spreading splits joined emoji and combining marks.

open as a page

Two strings that both display as "café" compare false with === in JavaScript, and one reports length 4 while the other reports 5. What is going on, and how do you compare them correctly?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Unicode allows two encodings of the same accented text: a precomposed é (U+00E9) or an e followed by a combining acute accent (U+0301). JavaScript's === compares code units, so the two differ. Call normalize('NFC') on both before comparing or storing.

open as a page

JavaScript's well-known symbols, such as `Symbol.iterator` and `Symbol.toStringTag`, key the language's built-in extension hooks. Why did the specification use symbol keys for these hooks instead of ordinary string names?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Symbol keys let the specification add new hooks that no existing code can already have defined. A string name like 'iterator' might collide with a property some object already uses, silently changing its behaviour; a fresh symbol cannot collide, so opting in is always deliberate.

open as a page

A tagged template can escape interpolated values while a plain template literal cannot. Why is that structurally true, and what does an escaping tag still fail to protect you from?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A tag receives the author-written text and the runtime values as separate arguments, so it can transform only the values — a plain template has already concatenated them and cannot tell them apart. The remaining gap is context: escaping that is right inside element text is wrong inside an attribute, a URL, or a script.

open as a page

What is the difference between `Symbol('app.id')` and `Symbol.for('app.id')` in JavaScript?

level: middleimportance: nice to knowfreq 33%

basics

~20 s

Symbol('app.id') creates a brand-new unique symbol every call. Symbol.for('app.id') looks the string up in a global registry shared across realms and returns the same symbol every time, so independent code can agree on one key.

open as a page

What does JavaScript's built-in String.raw tag do to a template literal, and what is it used for?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

String.raw is a built-in tag function that returns the template's text with backslash escape sequences left uninterpreted, so String.rawa\nb is four characters, not three. It exists for text where a backslash means a backslash: Windows paths, regex sources, and generated code.

open as a page

A JavaScript tagged template sits inside a function that is called many times. What can the tag function assume about the identity of the strings array it receives on each call, and how do libraries exploit that?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Each tagged-template call site has one template object, reused on every execution, and it is frozen. A tag can therefore use that array as a stable cache key — typically in a WeakMap — to parse or compile the static text once per site rather than once per call.

open as a page

A product rule says a display name may be at most 30 characters, and it has to be enforced in a browser form, in an API, and in storage. In JavaScript, which unit of "character" do you standardise on, and how do you keep the layers from disagreeing?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Pick grapheme clusters as the user-facing unit, normalise to NFC before counting, implement the rule in one shared function that every layer calls, and add a separate generous byte ceiling so unbounded combining marks cannot exhaust storage.

open as a page