How to Reverse a String in JavaScript (Without Breaking Emoji)
Learn how to reverse JavaScript strings with code points, grapheme clusters, Intl.Segmenter, word order, and line endings—without splitting emoji.
To reverse a string in JavaScript, you can often start with a one-line function—but only after deciding what a “character” means. For ASCII text, reversal is straightforward. The trouble starts with something a reader can see as one symbol: an emoji, an accented letter, or a family emoji made from several Unicode pieces.
The right implementation depends on what you want to reverse:
- UTF-16 code units: almost never the right user-facing choice.
- Unicode code points: a good lightweight fallback that keeps surrogate pairs together.
- Grapheme clusters: the closest match to user-perceived characters, including joined emoji and many combining-mark sequences.
- Words, lines, or records: a different operation entirely, with its own whitespace and newline rules.
This guide builds each version, explains the trade-offs, and ends with tests you can run in Node.js.
The short answer: use Array.from for code points
For modern JavaScript, this is the compact, code-point-safe pattern:
function reverseCodePoints(text) {
return Array.from(text).reverse().join("");
}
console.log(reverseCodePoints("A😀B")); // B😀A
The spread equivalent is just as clear:
const reversed = [..."A😀B"].reverse().join("");
console.log(reversed); // B😀A
Both forms consume a string through its iterator. As MDN documents for the String iterator, string iteration yields Unicode code points, so a supplementary-plane emoji is kept as one item. This is usually the best answer when you need a small dependency-free function and do not need full grapheme-cluster handling.
Why split('').reverse().join('') can break emoji
JavaScript strings are sequences of UTF-16 code units. The familiar one-liner splits at the boundaries of those code units, not necessarily at Unicode code-point boundaries:
const text = "A😀B";
const broken = text.split("").reverse().join("");
console.log(broken); // B��A
console.log([...broken].length); // 4
The 😀 character is represented by a high-surrogate and a low-surrogate. split('') separates those two values; reversing the array swaps them, leaving an invalid surrogate order. Many consoles display the result as replacement diamonds, but the underlying problem is data corruption, not merely a font issue.
For plain English letters and numbers, the one-liner appears to work because those characters fit in one UTF-16 code unit. That apparent success is why it survives in snippets and coding exercises. Do not use it for text that may contain emoji or other supplementary Unicode characters.
Array.from(text) and [...text] fix that specific surrogate-pair problem. They do not promise that every visible symbol stays intact. For example, a skin-tone emoji modifier or a zero-width-joiner family sequence can contain several code points that a reader perceives as one character.
When you need user-perceived characters: Intl.Segmenter
A grapheme cluster is an approximation of one user-perceived character. It may be one code point (A), a base plus a combining mark (é), or a multi-code-point emoji sequence (👨👩👧👦). Use Intl.Segmenter with granularity: "grapheme" when preserving those clusters matters.
function reverseGraphemes(text, locale = "en") {
if (typeof Intl.Segmenter !== "function") {
// Safe for surrogate pairs, but not guaranteed to preserve graphemes.
return Array.from(text).reverse().join("");
}
const segmenter = new Intl.Segmenter(locale, {
granularity: "grapheme",
});
const graphemes = Array.from(segmenter.segment(text), item => item.segment);
return graphemes.reverse().join("");
}
console.log(reverseGraphemes("A😀B")); // B😀A
console.log(reverseGraphemes("👨👩👧👦 A")); // A 👨👩👧👦
MDN describes Intl.Segmenter as locale-sensitive segmentation, and its segment() method returns an iterable of segments. The explicit mapping to item.segment is useful because the iterable yields segment records, not bare strings.
Intl.Segmenter is Baseline 2024 according to MDN: it works across current devices and browsers, but older runtimes may not have it. The fallback above is deliberately modest. It is code-point-safe only; it should not be described as a grapheme guarantee. If exact grapheme behavior is a requirement, decide whether to require a supported runtime or ship a well-maintained polyfill rather than silently promising equivalent behavior.
The locale argument can matter for segmentation rules. For ordinary emoji reversal, "en" (or the user’s locale) is a sensible default. This function reverses graphemes, not words or sentences.
Normalization does not solve every reversal problem
Unicode normalization can make canonically equivalent text use a more consistent representation, but it is not a substitute for segmentation. For example, é (an e followed by a combining acute accent) may normalize to a precomposed é when that form exists; it does not turn an arbitrary sequence into a single universal “character” in every visual or language-specific case.
If your application needs normalization, make it an explicit policy and test it separately:
function reverseNormalizedGraphemes(text, locale = "en") {
const normalized = text.normalize("NFC");
return reverseGraphemes(normalized, locale);
}
console.log(reverseNormalizedGraphemes("Café")); // éfaC
Normalization can also change the exact code-unit sequence, so do not apply it casually when preserving input bytes, identifiers, or user-entered spelling is important. Reversal itself is not normalization.
Strings do not mutate; arrays do
A JavaScript string is immutable. None of these functions changes the original text; each creates an array and joins a new string. The temporary array does change during .reverse().
That distinction matters when you are reversing an array you already own. Array.prototype.reverse() reverses in place and returns the same array reference:
const letters = ["a", "b", "c"];
const sameArray = letters.reverse();
console.log(sameArray === letters); // true
console.log(letters); // ["c", "b", "a"]
To keep the original array unchanged, copy first (or use toReversed() where your target runtime supports it):
const original = ["a", "b", "c"];
const copyReversed = [...original].reverse();
console.log(original); // ["a", "b", "c"]
console.log(copyReversed); // ["c", "b", "a"]
This is separate from Unicode safety: copying an array prevents mutation, but it does not repair an array that was created by splitting UTF-16 code units.
Reverse words without scrambling their letters
Sometimes “reverse a string” means reverse the order of words while leaving every word readable. First define whitespace behavior. The following function trims leading and trailing whitespace, treats runs of whitespace as separators, and emits one ordinary space between words:
function reverseWordOrder(text) {
return text.trim().split(/\s+/u).filter(Boolean).reverse().join(" ");
}
console.log(reverseWordOrder(" JavaScript handles words "));
// words handles JavaScript
This intentionally normalizes whitespace. It does not preserve tabs, multiple spaces, or line breaks. An empty or whitespace-only input returns an empty string because of filter(Boolean).
If preserving exact separators matters, do not use this convenience function. You need a tokenizer that stores words and whitespace as separate tokens, then reverses only the word tokens; punctuation policy also needs to be specified. For a ready-made conceptual comparison, see how to reverse words without scrambling the letters. For Python-specific approaches, see reverse a string in Python.
A related request is “reverse letters inside each word.” That is not the same operation as reversing word order, and it immediately raises the same Unicode question. If you need it, segment each word into graphemes, reverse those graphemes, and keep the separators in place rather than applying split('').
Reverse line order while preserving line contents
For logs, pasted lists, or text editors, you may want the last line first while leaving every line untouched. Input may use LF (\n), CRLF (\r\n), or old-style CR (\r). This version recognizes all three and preserves the sequence of separators, including a final line ending:
function reverseLineOrder(text) {
const finalEndingMatch = text.match(/(\r\n|\r|\n)$/u);
const finalEnding = finalEndingMatch ? finalEndingMatch[1] : "";
const body = finalEnding ? text.slice(0, -finalEnding.length) : text;
const parts = body.split(/(\r\n|\r|\n)/u);
const lines = [];
const endings = [];
for (let i = 0; i < parts.length; i += 2) {
lines.push(parts[i]);
if (i + 1 < parts.length) endings.push(parts[i + 1]);
}
lines.reverse();
let result = "";
for (let i = 0; i < lines.length; i++) {
result += lines[i];
if (i < endings.length) result += endings[i];
}
return result + finalEnding;
}
console.log(JSON.stringify(reverseLineOrder("one\r\ntwo\nthree")));
// "three\r\ntwo\none"
console.log(JSON.stringify(reverseLineOrder("one\r\ntwo\n")));
// "two\r\none\n"
The function reverses line content but keeps the sequence of separators in their original positions. That means a mixed-ending input remains mixed; it does not convert line endings. If your product instead defines “lines” as normalized records, normalize endings first and document that choice.
Test the behavior that matters
A deterministic test suite should include ordinary text, an astral emoji, a joined emoji, a combining mark, empty input, and immutability. These Node.js assertions exercise the character-level implementations and the word/line helpers. Run them in a runtime with Intl.Segmenter to check the grapheme-specific expectations:
const assert = require("node:assert/strict");
assert.equal(reverseCodePoints("hello"), "olleh");
assert.equal(reverseCodePoints("A😀B"), "B😀A");
assert.equal(reverseGraphemes("A😀B"), "B😀A");
assert.equal(reverseGraphemes("👨👩👧👦 A"), "A 👨👩👧👦");
assert.equal(reverseGraphemes("e\u0301x"), "xe\u0301");
assert.equal(reverseCodePoints(""), "");
assert.equal(reverseGraphemes(""), "");
assert.equal(reverseWordOrder(" one two "), "two one");
assert.equal(reverseWordOrder(" "), "");
assert.equal(reverseLineOrder("one\r\ntwo\nthree"), "three\r\ntwo\none");
const input = "A😀B";
reverseCodePoints(input);
assert.equal(input, "A😀B");
console.log("All reversal tests passed");
Run a file containing the functions and tests with node reverse.js. Keep the test expectations explicit: code-point reversal and grapheme reversal are different contracts, and adding a new kind of emoji is a useful regression test.
Put the function into a real interface safely
A reversal helper should have a clear input contract. The functions above expect a string, not an array, an object, or a missing value. A text input’s value is already a string. If you accept data from an API instead, validate the field before calling the helper. Automatically converting every value to a string can hide a bug: a missing value might become the literal word “undefined” instead of an empty result. Decide whether invalid input should cause an error or produce a documented default.
Keep the original text separate from the result. This lets users compare the two, change the reversal mode, and undo a transformation without reconstructing their input. Display the result with a text-only DOM property such as textContent or a textarea’s value, rather than inserting user text as HTML. Reversing text does not sanitize it, and a display feature should not accidentally interpret pasted markup.
For a short caption, the array allocation in these examples is usually unremarkable. For a large document, each conversion creates arrays and a new output string, so avoid recalculating on every tiny UI update without considering responsiveness. If you repeatedly segment text with the same locale and options, you can create one segmenter and reuse it. Measure realistic inputs instead of assuming that the shortest snippet is always the fastest.
Finally, grapheme reversal is still not a language-aware rewriting operation. Right-to-left text and mixed-direction content have display behavior beyond array order; punctuation may look surprising even when clusters stay intact. Test the actual output in the intended interface, not only a console. Use examples from your users’ languages, and make the chosen operation explicit: reversing visible character clusters does not translate a sentence or preserve its meaning.
Which function should you choose?
Use Array.from(text).reverse().join("") (or spread) for a compact, dependency-free reversal that will not split surrogate pairs. Use Intl.Segmenter when the output is user-facing and preserving perceived characters is important. Use a word-order or line-order function when “reverse” describes structure rather than individual characters.
For a quick one-off transformation, you can also use the Text Reverser tool. Whichever route you choose, remember that reversed text is only transformed text—not encryption or a meaningful security boundary. Never use reversal to protect passwords, tokens, personal data, or secrets.
Finally, consider accessibility. Reversing visible text can make screen-reader output confusing, and decorative captions should not rely on reversed text to convey essential meaning. Provide a short plain-text alternative or an accessible label when the reversed display is decorative, playful, or otherwise not the clearest representation.
Try the Text Reverser tool
Reverse text, words, or letters instantly — free and entirely in your browser.
Open the tool