JavaScript and Unicode: The UCS-2 Legacy Problem
JavaScript's string handling has a quirk that surprises many developers: strings are sequences of UTF-16 code units, not Unicode code points. This is a legacy of the language's 1995 design, when Unicode was expected to fit within 65,536 characters and UCS-2 (a fixed 2-bytes-per-character predecessor to UTF-16) was a reasonable choice.
The Code Unit Confusion
The String.prototype.length property returns the number of UTF-16 code units, not characters. For characters in the Basic Multilingual Plane (U+0000–U+FFFF), one character equals one code unit, so "hello".length === 5. But the 😀 emoji (U+1F600) is a supplementary character encoded as two UTF-16 code units: "😀".length === 2. This also affects methods like charAt(), charCodeAt(), and array-style indexing.
ES2015 Solutions
ECMAScript 2015 added proper Unicode support. String.prototype.codePointAt(i) returns the code point (not code unit) at a position. String.fromCodePoint() creates a string from code points. The for...of loop and the spread operator ([..."😀"]) iterate over code points rather than code units. The /u flag on regular expressions enables Unicode mode, where . matches supplementary characters and \u{1F600} syntax is available.
Checking String Length Correctly
To count Unicode characters (code points) rather than code units: [...str].length or Array.from(str).length. For grapheme clusters (what users perceive as "characters", including ZWJ sequences and emoji with skin tone modifiers), ES2021 introduced Intl.Segmenter.
Practical Impact
These issues matter most for user-facing string length limits (e.g., a 140-character tweet), string reversal (naively reversing a string breaks surrogate pairs), and regular expressions that need to match emoji. Understanding UTF-16 encoding is key to writing correct JavaScript string code. Use our character browser to check whether a specific character falls in the BMP or a supplementary plane.