JavaScript Emoji: String Length and Safe Truncation
Checking '🚀'.length in JavaScript yields 2! Using str.substring(0, 1) splits surrogate pairs, producing corrupted replacement boxes (). This article explores Unicode string algorithms and grapheme-safe truncation.
1. Problem: '🚀'.length === 2 counter-intuitive behavior and broken truncation
Restricting user input length (such as 100 characters in comment forms) using str.length and str.substring(0, N) causes frequent truncation bugs. Character 'A' has a length of 1, but Emoji '🚀' has a length of 2, and composite Emojis (like family '👨👩👧👦') reach lengths of 11! Slicing through a surrogate pair or Zero-Width Joiner (ZWJ) leaves an isolated surrogate pair that renders as replacement boxes ().
2. Minimal reproduction: length calculation and slicing pitfalls
This snippet illustrates Emoji length anomalies in JavaScript and the corrupted output generated by naive string slicing:
/* 1. Emoji Length Variance */
console.log("🚀".length); // Output: 2 (High surrogate \uD83D + Low surrogate \uDE80)
console.log("👨👩👧👦".length); // Output: 11 (4 Emoji code points + 3 ZWJ joiners \u200D)
/* 2. Naive Slicing Yields Isolated Surrogate Corruption */
const text = "Hello 🚀 World";
const truncated = text.substring(0, 7); // Cuts right in the middle of '🚀'!
console.log(truncated); // Output: "Hello \uD83D" -> isolated high surrogate renders as !3. Root cause: Code Units, Code Points, and Grapheme Clusters
1. Code Unit: JavaScript stores strings internally using UTF-16. Each Code Unit is 16 bits. .length counts UTF-16 Code Units, not human-perceived characters. 2. Code Point: the unique numerical identifier assigned to a symbol in Unicode (U+0000 to U+10FFFF). Supplementary plane characters (such as Emoji 🚀 U+1F680) require a 32-bit representation composed of a UTF-16 Surrogate Pair. 3. Grapheme Cluster: a single user-perceived character. For instance, '👨👩👧👦' combines 7 Unicode code points via ZWJs into one visual symbol. 4. `Array.from` Limitations: Array.from(str) or [...str] correctly splits single surrogate pair code points, but fails on complex ZWJ Emojis, breaking them into separate components.
4. Recommendation: adopting Intl.Segmenter for grapheme-aware safe truncation
1. Modern Standard: use native Intl.Segmenter (specifying granularity: 'grapheme') available in ES2022 to accurately parse complex Unicode Grapheme Clusters. 2. Fallback Strategy: fallback to Array.from() for single-code-point Emojis, ensuring slice boundaries never separate a High Surrogate (0xD800 sim 0xDBFF) from its Low Surrogate.
5. Complete code: Intl.Segmenter Unicode safe truncation helper function
This TypeScript snippet demonstrates building a grapheme-aware, zero-corruption Unicode truncation function.
function safeUnicodeTruncate(str: string, maxGraphemes: number): string {
if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const segments = Array.from(segmenter.segment(str));
if (segments.length <= maxGraphemes) {
return str;
}
return segments
.slice(0, maxGraphemes)
.map((s) => s.segment)
.join("");
}
const codePoints = Array.from(str);
if (codePoints.length <= maxGraphemes) {
return str;
}
return codePoints.slice(0, maxGraphemes).join("");
}
const longTitle = "Breaking 🚀 👨👩👧👦 Story";
console.log("Safe Truncate 4 Graphemes:", safeUnicodeTruncate(longTitle, 4));6. Incorrect approaches
Using str.substring(0, N) or str.slice(0, N) on text containing Emojis. Assuming all Emojis have a length of 2 (ignoring ZWJ sequences and skin-tone modifiers). Assuming regex . matches all Unicode characters (without the /u flag, . misses surrogate pairs).
7. Boundaries: Regex /u / /v flags and database charset requirements
Regex operations must enable the /pattern/u (or ES2024 /pattern/v) flag to treat surrogate pairs as single characters. Databases like MySQL require utf8mb4; legacy 3-byte utf8 (utf8mb3) fails when storing 4-byte Emojis.
8. Verification
Inspect strings for isolated surrogates using regex /[í €-í¯¿](?![í°€-í¿¿])/. Verify that complex ZWJ Emojis (👨👩👧👦) and skin-tone modifiers (👍🏽) maintain visual integrity after truncation.
9. FAQ
Why does Array.from('👨👩👧👦').length return 7? Because the family Emoji combines 4 individual person code points connected by 3 ZWJ characters (); Array.from splits strings by Code Points into 7 elements.
What is Intl.Segmenter browser support? Chrome 87+, Safari 14.1+, Firefox 125+, and Node.js 16+ fully support Intl.Segmenter.
How do I check string length by byte size for API payloads? Measure byte length using new TextEncoder().encode(str).length to prevent payload buffer overflow.
10. Summary
Understanding Code Units, Code Points, and Grapheme Clusters prevents Unicode string bugs. Avoid naive substring operations and adopt Intl.Segmenter or Array.from for grapheme-safe string handling.
Sources and further reading
Specifications and references for checking the article’s technical details.
- The Unicode Standard
Unicode Consortium
- Encoding Standard
WHATWG
Related articles
Unicode: UTF-8, UTF-16 and Surrogate Pairs
Compare Unicode character sets with UTF-8 / UTF-16 encodings, detailing High/Low Surrogate pair bitwise formulas and binary conversions.
TroubleshootingGarbled Text: Charset Diagnosis and Recovery Limits
Systematically diagnose UI garbled text (un-decoded \u4e2d, Latin1 mismatches), HTTP header omissions, and MySQL latin1 corruptions.
ConceptsJSON Unicode Escapes: UTF-16 and UTF-8
Understand JSON \uXXXX code units, emoji surrogate pairs, native Unicode, escaped text, and safe conversion or validation.
Keep exploring
Use the related browser tool to test the workflow with your own sample data.
Open related tool