Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. How to reverse a string in JavaScript
Learn

How to reverse a string in JavaScript

To reverse a string in JavaScript, use [...str].reverse().join(''). The common split('') version breaks emoji, and only Intl.Segmenter handles flags.

By Max Arthur
Co-Founder & Content MarketerΒ·October 17, 2026Β·9 Min read
White text reading 'How to reverse a string in JavaScript' on a dark purple dotted background

JavaScript strings have no reverse() method, so the standard way to reverse a string in JavaScript is to turn it into an array, reverse the array, and join it back together:

const reverse = (str) => [...str].reverse().join("");

reverse("hello");   // "olleh"

Spreading the string with [...str] gives an array of characters, reverse() flips that array, and join("") glues the characters back together with nothing between them. Three steps, one line, and the original string is untouched.

You will often see the same thing written with split("") instead of the spread:

"hello".split("").reverse().join("");   // "olleh"

On plain English text the two give identical results. They stop agreeing the moment the string contains an emoji, which is the part most tutorials leave out and the part this post spends the most time on. Every output below was measured in Node v24.14.1.

Why strings have no .reverse()

Arrays have reverse(). Strings do not:

typeof "abc".reverse;   // "undefined"
"abc".reverse();        // TypeError: "abc".reverse is not a function

The reason is that strings in JavaScript are immutable. Array reverse() works by rearranging the array in place and handing back that same array. No string method can change the string it was called on, so a string has nothing to rearrange. Every string method returns a new string instead.

That is why reversing has to go through an array: make a copy you are allowed to change, change it, then turn it back into a string.

It also answers a common interview question. You cannot reverse a string in place in JavaScript. Any solution that claims to is really reversing an array of characters, and that array is already a copy.

How split, reverse and join work together

Taking the one-liner apart makes each step visible:

const chars = "hello".split("");   // ["h", "e", "l", "l", "o"]
chars.reverse();                   // ["o", "l", "l", "e", "h"]
chars.join("");                    // "olleh"

split("") with an empty separator cuts the string between every character. reverse() mutates the array and also returns it, which is what lets the calls chain on one line. join("") with an empty separator puts the pieces back with nothing in between.

Leave the argument out of join and it uses a comma, giving "o,l,l,e,h". That is the most common typo in this one-liner, and it is easy to miss in a quick test.

If you would rather not mutate anything, toReversed() (added in ES2023) returns a new array and leaves the original alone:

[..."abc"].toReversed().join("");   // "cba"

For a throwaway array created on the same line it makes no practical difference. It reads more honestly when the array has a name and is used again later. Both split and join have more edge cases than this one; the full list is in split() and join() in JavaScript.

Where reversing a string breaks: emoji, flags and accents

Here is the result that makes split("") the wrong default:

"Hello πŸ˜€".split("").reverse().join("");   // "\ude00\ud83d olleH"

The emoji did not move to the front. It was cut in half and the halves were swapped, leaving two orphaned codes that display as replacement boxes or nothing at all.

The cause is how JavaScript stores text. A string is a sequence of UTF-16 code units, and "Hello πŸ˜€".length is 8, not 7, because the emoji takes two code units, called a surrogate pair. split("") splits by code unit, so it splits the pair. The same problem shows up when you capitalize the first letter of a string, where charAt(0) returns half an emoji.

Broken output is not only ugly. The reversed string is no longer valid Unicode, and the APIs that check for that will refuse it:

const broken = "πŸ˜€".split("").reverse().join("");

broken.isWellFormed();        // false
encodeURIComponent(broken);   // URIError: URI malformed

So a reverse feature that receives user text can throw inside a URL builder three functions later, a long way from the line that caused it.

Spreading fixes the simple case, because a string's iterator walks by code point and keeps a surrogate pair together. But a code point is still not what a reader thinks of as one character. Here is what each method did to five strings:

Input split("") [...str] Intl.Segmenter
"Hello πŸ˜€" broken "πŸ˜€ olleH" "πŸ˜€ olleH"
"πŸ‘πŸ½ok" broken "koπŸ½πŸ‘" "koπŸ‘πŸ½"
"πŸ‡ΊπŸ‡ΈπŸ‡«πŸ‡·" broken "πŸ‡·πŸ‡«πŸ‡ΈπŸ‡Ί" "πŸ‡«πŸ‡·πŸ‡ΊπŸ‡Έ"
"πŸ‘¨β€πŸ‘©β€πŸ‘§" broken "πŸ‘§β€πŸ‘©β€πŸ‘¨" "πŸ‘¨β€πŸ‘©β€πŸ‘§"
"café" accent moves accent moves "éfac"

Each row below the first is a different way that several code points add up to one visible character:

  • Skin tones. πŸ‘πŸ½ is a thumbs up followed by a separate tone modifier. Spread reverses the two, so the modifier ends up stranded in front of the thumb.
  • Flags. A flag is two regional-indicator letters. πŸ‡ΊπŸ‡ΈπŸ‡«πŸ‡· is the letters U, S, F and R. Reversed by code point they become R, F, S and U, which pair up as different flags or as no flag at all.
  • Joined emoji. The family emoji is three people glued together with invisible zero-width joiners. Spread keeps the glue in place but reverses the family.
  • Combining accents. "café" stores the Γ© as a plain e followed by a combining accent. Reversed, the accent moves to the front and has nothing to sit on.

Only the last column gets every row right.

Reverse a string by character with Intl.Segmenter

What a reader calls a character, Unicode calls a grapheme cluster, and Intl.Segmenter is the built-in that finds them:

const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });

const reverse = (str) =>
  [...segmenter.segment(str)]
    .map(({ segment }) => segment)
    .reverse()
    .join("");

reverse("πŸ‘πŸ½ok");   // "koπŸ‘πŸ½"
reverse("πŸ‡ΊπŸ‡ΈπŸ‡«πŸ‡·");   // "πŸ‡«πŸ‡·πŸ‡ΊπŸ‡Έ"

segment() returns an iterable of objects, each with a segment property holding one grapheme. Spread it into an array, keep only the text, and the rest is the familiar reverse and join. Intl.Segmenter is built into every current browser and into Node, so there is nothing to install. MDN's Intl.Segmenter reference covers the word and sentence modes too.

It is slower. Reversing 100,000 characters took 17ms with the segmenter against 2.6ms with spread, roughly six times as long. For a username or a chat message that difference is invisible. For a megabyte of log text, decide whether emoji correctness is worth it.

A sensible rule: if the text is yours (identifiers, hex strings, fixed test data), [...str] is fine. If the text came from a person, use the segmenter.

Other ways to reverse a string: loop, reduce, recursion

How do you reverse a string without reverse()?

Interviewers ask this to see whether you can do it by hand. A loop that walks backwards from the last index is the clearest answer:

function reverse(str) {
  let out = "";
  for (let i = str.length - 1; i >= 0; i--) {
    out += str[i];
  }
  return out;
}

reduce can do the same by putting each character in front of everything collected so far:

const reverse = (str) => [...str].reduce((acc, ch) => ch + acc, "");

Recursion reverses the rest of the string, then adds the first character on the end:

const reverse = (str) => (str === "" ? "" : reverse(str.slice(1)) + str[0]);

The recursive version is short and has a hard limit. Measured, it handled 1,000 characters and threw RangeError: Maximum call stack size exceeded at 10,000, because every character costs one stack frame. Treat it as an exercise, not a tool. The same limit is behind most of the surprises in recursion explained.

One more difference that is easy to miss: the loop reads str[i], which is a code unit, so it splits emoji exactly like split("") does. The reduce version spreads first, so it does not.

Here is how they compared on a 1,000,000-character string, 20 runs each:

Method Time for 20 runs
backwards for loop 144ms
reduce with spread 370ms
split("").reverse().join("") 594ms
[...str].reverse().join("") 640ms

The loop wins because it never builds an intermediate array. But even the slowest reversed a million characters in about 32ms per run. Unless reversing strings is genuinely your hot path, choose on correctness and readability. The Big O notation post explains why a constant-factor gap like this one rarely matters next to a change in growth rate.

Reverse the words in a string

"Reverse a string" sometimes means reversing the order of the words rather than the letters. Split on spaces, reverse the array of words, and join with spaces:

"hello big world".split(" ").reverse().join(" ");   // "world big hello"

Or keep the word order and reverse each word on its own, by mapping the reverse function over the words:

"hello big world".split(" ").map(reverse).join(" ");   // "olleh gib dlrow"

Both versions assume single spaces between words. With messy input, split on /\s+/ after trimming instead, or the empty strings from double spaces end up in the result.

Check for a palindrome

The most common real use of string reversal is checking whether text reads the same in both directions:

const isPalindrome = (str) => {
  const clean = str.toLowerCase().replace(/[^a-z0-9]/g, "");
  return [...clean].reverse().join("") === clean;
};

isPalindrome("racecar");                          // true
isPalindrome("A man, a plan, a canal: Panama");   // true

The cleaning step does most of the work. Without it, "A man, a plan, a canal: Panama" is not equal to its own reverse, because the capital A, the commas and the spaces all land in different positions. Lowercase first, strip everything that is not a letter or a digit, then compare.

The [^a-z0-9] character class only knows ASCII, so it throws away accented letters along with the punctuation. For text in other languages, use /[^\p{L}\p{N}]/gu instead, which keeps any letter or number in any script:

const clean = (str) => str.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");

clean("Γ‰sΓ©!");   // "Γ©sΓ©"

Quick reference

[...str].reverse().join("")              // by code point: fine for most text
str.split("").reverse().join("")         // by code unit: breaks emoji
[...str].toReversed().join("")           // same, without mutating the array

// by grapheme: flags, skin tones, accents
const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
[...seg.segment(str)].map((s) => s.segment).reverse().join("");

Want to practise string handling where the code actually runs? Start the JavaScript track.

Keep reading

More from the blog

White text reading 'What if name equals main does in Python' on a dark purple dotted background
October 27, 2026Β·8 min readLearn

What if __name__ == '__main__' actually does in Python

In Python, if __name__ == '__main__' runs code only when you run the file directly, not when it is imported. What __name__ holds and when you need it.

Read more
White text reading 'How to reverse a list in Python' on a dark purple dotted background
October 26, 2026Β·8 min readLearn

How to reverse a list in Python (and a string)

To reverse a list in Python, use reverse() to change it in place, [::-1] for a reversed copy, or reversed() to loop backwards. Which to use and why.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
Β© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons