Why Reversing Text Breaks Emoji — The Surrogate Pair Trap
If you've ever reversed "Hi😀!" and gotten a broken box (□) or a question mark instead of the emoji, that wasn't a random bug — it's a predictable trap rooted in how JavaScript strings are stored internally. This guide explains why naively written string-reversal code breaks emoji, and how to fix it.
1. The real unit of a JavaScript string: the UTF-16 code unit
JavaScript strings aren't stored in units of "characters" the way people think of them — they're stored in UTF-16 code units. Letters, most Hangul, and basic punctuation are each represented by a single code unit, but most emoji and some rare CJK characters have Unicode code points beyond U+FFFF (the Supplementary Plane), which means it takes two code units paired together to represent one. This pair is called a surrogate pair. Only when a High Surrogate and a Low Surrogate appear stuck together in the correct order does the browser render them as a single emoji.
2. Where it breaks: split('') and .length
A lot of string-reversal code is written as str.split('').reverse().join(''). Here, split('') splits the string by "code unit" — not by the "character" a human perceives. If an emoji is made of two code units (a surrogate pair), split('') forcibly splits that pair into two separate array elements. Once .reverse() runs, the high and low surrogates that used to be adjacent end up separated or in the wrong order — either way, the pairing is now invalid. An unpaired surrogate doesn't point to a valid character, so the browser displays it as a broken glyph (a box, a replacement character, etc.).
"Hi😀!" splits by code unit into 5 pieces: ['H','i', the high surrogate of 😀, the low surrogate of 😀, '!']. Reverse that directly and you get ['!', low surrogate, high surrogate, 'i', 'H'] — the low surrogate now sits before the high surrogate, which is not a valid surrogate sequence, so the emoji breaks.
3. The correct fix: reversing by code point
The ECMAScript (JavaScript standard) spec defines the [...str] spread syntax and Array.from(str) to iterate a string by code point. That means they automatically recognize surrogate pairs and treat each one as a single unit. Switching to [...str].reverse().join('') moves each surrogate pair as an intact unit, preserving the high/low order and keeping the emoji intact.
| Method | Splits by | Emoji-safe? |
|---|---|---|
str.split('') | UTF-16 code unit | ❌ Risk of splitting surrogate pairs |
[...str] / Array.from(str) | Unicode code point | ✅ Preserves surrogate pairs |
Intl.Segmenter | Grapheme cluster (what a human sees as "one character") | ✅✅ Preserves ZWJ-composed emoji too |
4. What code-point reversal still can't fix: grapheme clusters
[...str] isn't a silver bullet, though. A flag emoji (🇰🇷) is rendered as a single flag by placing two Regional Indicator code points side by side — but those two are separate code points, not a surrogate pair, so a code-point-based reversal still treats them independently. The same goes for family emoji (👨👩👧👦), which combine several emoji into one using a ZWJ (zero-width joiner). To correctly preserve these "true single characters as a human sees them" (grapheme clusters), you need the more modern Intl.Segmenter API, which splits text at a level one step higher than code points.
5. How this tool handles it
Modoohub's text reverser tool's actual source code uses [...input].reverse().join('') for reversing the whole text, and [...seg].reverse().join('') for reversing each word individually (word-reverse mode). In other words, it uses the spread operator for code-point-based reversal from the start, rather than split(''), so ordinary emoji (the kind built from surrogate pairs) stay intact. That said, as explained above, grapheme clusters made of multiple code points — like flag emoji or ZWJ-composed emoji — can still end up out of order without Intl.Segmenter. That's a limitation shared by any code-point-based reversal approach, this tool included.
FAQ
Q. Why does this problem only show up with emoji?
A. Most letters and Hangul characters have Unicode code points at or below U+FFFF, so they're represented by a single UTF-16 code unit. Most emoji, on the other hand, have code points beyond U+FFFF (for example, 😀 is U+1F600), requiring two code units — a surrogate pair. So this problem only surfaces in operations that slice by code unit.
Q. Why is counting emoji with .length wrong?
A. .length returns the number of code units, so a single emoji made of a surrogate pair counts as length 2. "😀".length is 2, not 1. To count actual "characters" accurately, use [...str].length or Intl.Segmenter.
Q. Does Modoohub's text reverser have this bug?
A. No. This tool implements code-point-based reversal with the [...text] spread operator from the ground up, so ordinary emoji stay intact. The only exception is grapheme clusters made of multiple code points, like flag emoji or ZWJ-composed emoji, which can still end up out of order — a limitation shared by any code-point-based reversal approach.
Q. Does palindrome checking run into the same problem?
A. Yes. Palindrome-checking code built on split('') and reverse() can produce incorrect results for strings that contain emoji. It's safer to compare by code point or use a dedicated palindrome checker.