Reverse a String Safely
Reversing a string is the textbook one-liner, but the obvious version `[...str].reverse().join('')` corrupts emoji families, flag sequences, and any character with combining marks. This snippet starts with the naive UTF-16 reverse, upgrades to a code-point reverse that fixes surrogate pairs, and ends with `Intl.Segmenter` for true grapheme-cluster correctness. Pick the version that matches the worst-case input you actually expect.
1,103 views
5
function reverseNaive(str) {
return str.split('').reverse().join('');
}
console.log(reverseNaive('hello')); // olleh
console.log(reverseNaive('A man a plan a canal Panama'));
// amanaP lanac a nalp a nam A
console.log(reverseNaive('')); // (empty)split('') breaks the string into UTF-16 code units, reverse() flips the array in place, and join('') glues the code units back together. For pure ASCII, this is correct, fast, and exactly what every interview question expects. The hidden bug is that any character outside the Basic Multilingual Plane (most emoji, mathematical italics, several historical scripts) is stored as a surrogate pair of two code units, and naive reversal swaps the high and low halves, producing invalid UTF-16. Reach for this only when you can guarantee ASCII input, like reversing a number for a palindrome check.
function reverseCodePoints(str) {
return [...str].reverse().join('');
}
console.log(reverseCodePoints('hello')); // olleh
console.log(reverseCodePoints('I love \u{1F30D}!')); // !🌍 evol I (emoji intact)
console.log(reverseCodePoints('a\u{1D400}b')); // b𝐀a (math bold A intact)The spread operator on a string iterates by Unicode code point (it uses the string's [Symbol.iterator]), so each surrogate pair stays as one entry in the resulting array. Reversing that array and joining preserves multi-code-unit characters like the globe 🌍 (U+1F30D) and mathematical bold A 𝐀 (U+1D400). This handles 95% of real-world reverse-a-string needs cleanly with no extra dependencies. The remaining 5% is grapheme clusters, where one user-perceived character is actually multiple code points (combining accents, ZWJ-joined emoji, regional indicator pairs for flags), which the next accordion handles.
function reverseGraphemes(str) {
const seg = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
const graphemes = [];
for (const { segment } of seg.segment(str)) {
graphemes.push(segment);
}
return graphemes.reverse().join('');
}
// Combining mark: 'é' as 'e' + U+0301
const eCombining = 'cafe\u0301';
console.log(reverseGraphemes(eCombining)); // éfac (mark stays attached)
// ZWJ family emoji: man + ZWJ + woman + ZWJ + girl
const family = '\u{1F468}\u200D\u{1F469}\u200D\u{1F467}';
console.log(reverseGraphemes('AB' + family + 'YZ'));
// ZY👨👩👧BA (family stays as one cluster)A grapheme cluster is what a human sees as a single character: a base letter plus its combining marks, a country flag (two regional-indicator code points), or a multi-person emoji joined by zero-width joiners. [...str] still splits these into separate entries, so the spread version reverses inside the cluster. Intl.Segmenter with granularity: 'grapheme' walks the string in user-perceived characters, which is the only way to reverse without breaking emoji families or detaching combining accents. It is available in modern Node and all current browsers, and the cost is one allocation per call. Use it whenever the input is user-facing text in any language.
