Charts · 19

The four lengths of a string

A username field says “20 characters max” and rejects a 20-letter Vietnamese name. A preview truncates a tweet and leaves half an emoji. A sort puts two identical cafés in different places. All the same bug: a string has four lengths, and .length gives you the one nobody means. Type anything and watch the four counts split apart, then see the three ways to cut it.

One grapheme, four people joined by zero-width joiners: 7 code points, 11 UTF-16 units, 25 bytes.

graphemes1Intl.Segmenterwhat a person would count, and what backspace deletes
code points7Array.from(s).lengthUnicode scalar values, what a for…of loop yields
UTF-16 units11s.lengthwhat .length, .slice and .charAt actually count
UTF-8 bytes25new TextEncoder().encode(s).lengthwhat goes over the wire and into most databases
👨‍👩‍👧‍👦U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466

The “5 character limit”, three ways

s.slice(0, 5)

👨‍👩

UTF-16 units. Can split a surrogate pair and leave a broken half-character.
Array.from(s).slice(0, 5)

👨‍👩‍👧

Code points. Never splits a pair, still splits a family, a flag or an accent.
Intl.Segmenter … slice(0, 5)

👨‍👩‍👧‍👦

Graphemes. Cuts where a person would. This is the one to use for a limit.

Notes

JavaScript strings are sequences of UTF-16 code units, so .length, .slice, .charAt and index access all count in units: anything outside the Basic Multilingual Plane (every emoji, many CJK characters, historic scripts) is two units, a surrogate pair, and slicing between them yields a lone surrogate that renders as a broken box. Array.from and for…of iterate code points, which fixes pairs but not sequences: a flag is two code points, a skin-toned thumb is two, a family can be seven joined by U+200D. What a person calls a character is an extended grapheme cluster, defined by Unicode Standard Annex 29 and exposed as Intl.Segmenter, in every engine since 2024. UTF-8 bytes are what the wire and most databases count; a VARCHAR(20) in MySQL utf8mb4 is 20 characters but 80 bytes. The two cafés are normalisation forms NFC and NFD; compare with a.normalize() === b.normalize(), and prefer Intl.Collator for sorting. Sources: ECMAScript specification, String objects; Unicode Standard Annex 15 (Normalization) and 29 (Text Segmentation); ECMA-402, Intl.Segmenter.