UTF-16 Code Units vs Code Points in JavaScript

JavaScript internally encodes strings as sequences of 16-bit UTF-16 code units. This architectural design means that while characters within the Basic Multilingual Plane (BMP) map directly to a single code unit, supplementary characters—such as emojis, mathematical symbols, and historical scripts—require two code units, known as a surrogate pair. This article explores how JavaScript distinguishes between UTF-16 code units and Unicode code points, the common pitfalls with legacy string methods, and the modern features designed to handle full Unicode accurately.

Code Units vs. Code Points: The Core Difference

A Unicode code point is a numerical value assigned to a specific abstract character in the Unicode standard, ranging from U+0000 to U+10FFFF.

A UTF-16 code unit is a 16-bit binary value used to encode code points: * Basic Multilingual Plane (BMP): Covers code points from U+0000 to U+FFFF. These fit into a single 16-bit code unit. * Supplementary Planes: Cover code points from U+010000 to U+10FFFF. These cannot fit into 16 bits, so UTF-16 encodes them as a pair of 16-bit units called a surrogate pair (one high surrogate and one low surrogate).

Legacy JavaScript: Operating on Code Units

Historically, ECMAScript defined strings strictly as sequences of 16-bit elements. As a result, standard property lookups and legacy methods operate on UTF-16 code units rather than Unicode code points.

1. String Length

The .length property returns the number of UTF-16 code units, not the number of rendered characters or code points.

const char = "A"; // BMP: U+0041
console.log(char.length); // 1

const emoji = "😀"; // Supplementary: U+1F600 (encoded as \uD83D\uDE00)
console.log(emoji.length); // 2

2. Indexing and charAt()

Bracket notation (str[index]) and .charAt(index) return the code unit at the specified position. Accessing an index of a surrogate pair splits the character and returns an isolated, invalid surrogate.

const emoji = "😀";
console.log(emoji[0]); // "\uD83D" (High surrogate, cannot be displayed alone)
console.log(emoji.charAt(0)); // "\uD83D"

3. Character Codes

.charCodeAt() returns the 16-bit numeric value of the code unit at the given index.

const emoji = "😀";
console.log(emoji.charCodeAt(0)); // 55357 (0xD83D)
console.log(emoji.charCodeAt(1)); // 56832 (0xDE00)

Modern JavaScript: Code-Point Aware Features

ECMAScript 2015 (ES6) introduced native support for full Unicode code points across iterations, methods, and regular expressions.

1. codePointAt() and String.fromCodePoint()

To retrieve the complete 21-bit Unicode value, use codePointAt(). It reads both halves of a surrogate pair when called on the index of the leading surrogate. Conversely, String.fromCodePoint() converts code points into properly encoded strings.

const emoji = "😀";
console.log(emoji.codePointAt(0)); // 128512 (0x1F600)
console.log(String.fromCodePoint(0x1F600)); // "😀"

2. Iterators and Spread Syntax

JavaScript’s string iterator is code-point aware. Using for...of or the spread operator (...) iterates over complete Unicode code points rather than raw code units.

const text = "A😀B";

// Length in code units
console.log(text.length); // 4

// Length in code points using the spread operator
const codePoints = [...text];
console.log(codePoints); // ["A", "😀", "B"]
console.log(codePoints.length); // 3

3. Unicode Regular Expressions (u and v flags)

Using the u (Unicode) or v (Unicode Sets) flag in regular expressions ensures patterns match full code points instead of single code units.

// Without the 'u' flag, '.' matches a single code unit:
console.log(/^.$/.test("😀")); // false

// With the 'u' flag, '.' matches a full code point:
console.log(/^.$/u.test("😀")); // true

Summary Comparison

Operation Code Unit Level (16-bit) Code Point Level (Full Unicode)
Length Count str.length [...str].length or Array.from(str).length
Character Access str[i] / str.charAt(i) [...str][i]
Numeric Value str.charCodeAt(i) str.codePointAt(i)
String Generation String.fromCharCode(...) String.fromCodePoint(...)
Pattern Matching /^.$/ /^.$/u