Javascript is required
PitfallsPublished 2026-07-28Updated 2026-08-086 min read

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.

By: Tiny
UnicodeEmojiSurrogate PairsJavaScript Length

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.

Related articles

Keep exploring

Use the related browser tool to test the workflow with your own sample data.

Open related tool