JavaScript and Unicode: The UCS-2 Legacy Problem

· 2 min read

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.

More Articles

View all articles