Toolmingo
Guides7 min read

How to Reverse a String (With Unicode and Emoji Support)

Learn how to reverse a string in JavaScript, Python, Go, and PHP. Covers the naive approach, Unicode grapheme clusters, emoji pitfalls, and palindrome detection.

How to Reverse a String

Reversing a string is one of the most common interview questions and a surprisingly deep problem when you factor in Unicode. The basic idea — flip the order of characters — works perfectly for ASCII text. But once emoji, accented letters, or combined scripts enter the picture, a naive reversal produces broken output.

This guide covers the correct approach for every language, explains why Unicode makes this tricky, and shows how to extend reversal into related tasks like palindrome detection and word-order reversal.


The Naive Approach

The simplest method is to split the string into an array of characters, reverse the array, and join it back:

// JavaScript (ASCII only — breaks with emoji)
"hello".split("").reverse().join("") // "olleh"

This works perfectly for plain ASCII. The problem arises with multi-codepoint Unicode characters.


Why Unicode Breaks Naive Reversal

Modern text is not just bytes or even code points — it is grapheme clusters: what a user perceives as a single character. A single grapheme can span multiple Unicode code points:

Visual Code points Notes
é U+0065 + U+0301 e + combining acute accent (NFD form)
👨‍👩‍👧 5 code points joined by ZWJ Family emoji
🏳️‍🌈 4 code points + variation selector Rainbow flag
ñ U+00F1 (NFC) or U+006E + U+0303 (NFD) Depends on normalization

Reversing code points instead of grapheme clusters splits these characters apart, producing unrecognisable output:

"café".split("").reverse().join("")
// Might produce "éfac" correctly (NFC) or "́efac" broken (NFD)

"😀👍".split("").reverse().join("")
// "👍😀" — looks fine here, but only by luck (both BMP supplementary)

"👨‍👩‍👧".split("").reverse().join("")
// Complete garbage — ZWJ sequences destroyed

The Correct Approach: Grapheme-Aware Reversal

JavaScript

Use the Intl.Segmenter API (available in all modern browsers and Node.js 16+):

function reverseString(str) {
  const segmenter = new Intl.Segmenter();
  const graphemes = [...segmenter.segment(str)].map(s => s.segment);
  return graphemes.reverse().join("");
}

reverseString("café")       // "éfac" (correct)
reverseString("hello 👋")  // "👋 olleh" (correct)
reverseString("👨‍👩‍👧")       // "👨‍👩‍👧" (correct — family stays intact)
reverseString("hello")      // "olleh"

For environments without Intl.Segmenter, use the graphemer npm package:

import Graphemer from "graphemer";

function reverseString(str) {
  const splitter = new Graphemer();
  return splitter.splitGraphemes(str).reverse().join("");
}

Python

Python 3 strings are sequences of Unicode code points, so [::-1] works correctly for most text — but not for ZWJ sequences (multi-codepoint emoji):

# Works for most text
def reverse_string(s: str) -> str:
    return s[::-1]

print(reverse_string("hello"))   # "olleh"
print(reverse_string("café"))    # "éfac" ✓ (NFC composed)

For full grapheme-cluster support (including ZWJ emoji), use the grapheme package:

# pip install grapheme
import grapheme

def reverse_string_unicode(s: str) -> str:
    return "".join(reversed(list(grapheme.graphemes(s))))

print(reverse_string_unicode("👨‍👩‍👧"))  # "👨‍👩‍👧" — intact ✓

Quick rule: If your text contains emoji that might be ZWJ sequences (family, couple, profession emoji), use the grapheme package. For typical accented Latin text, [::-1] is fine.


Go

Go strings are byte slices, so you must work at the rune (code point) level at minimum. For full grapheme support, use golang.org/x/text/unicode/norm plus golang.org/x/text:

package main

import (
    "fmt"
    "unicode/utf8"
)

// Reverses by rune (code point) — correct for precomposed NFC text
func reverseRunes(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

func main() {
    fmt.Println(reverseRunes("hello"))       // olleh
    fmt.Println(reverseRunes("café"))        // éfac  ✓ (if NFC)
    fmt.Println(utf8.RuneCountInString("café")) // 4 runes
}

For ZWJ emoji, use the github.com/rivo/uniseg package:

import "github.com/rivo/uniseg"

func reverseGraphemes(s string) string {
    var graphemes []string
    gr := uniseg.NewGraphemes(s)
    for gr.Next() {
        graphemes = append(graphemes, gr.Str())
    }
    // Reverse
    for i, j := 0, len(graphemes)-1; i < j; i, j = i+1, j-1 {
        graphemes[i], graphemes[j] = graphemes[j], graphemes[i]
    }
    result := ""
    for _, g := range graphemes {
        result += g
    }
    return result
}

PHP

PHP strings are byte arrays. Use mb_str_split (PHP 7.4+) to split by character, not byte:

<?php
// Reverses by UTF-8 character (code point) — works for NFC text
function reverse_string(string $s): string {
    $chars = mb_str_split($s, 1, 'UTF-8');
    return implode('', array_reverse($chars));
}

echo reverse_string("hello");  // olleh
echo reverse_string("café");   // éfac ✓

For emoji-safe reversal, use grapheme_str_split (requires the intl extension):

<?php
function reverse_string_grapheme(string $s): string {
    // grapheme_str_split splits by grapheme cluster
    $graphemes = preg_split('/(\X)/u', $s, -1,
        PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
    return implode('', array_reverse($graphemes));
}

echo reverse_string_grapheme("👨‍👩‍👧");  // intact family emoji ✓

Common Variations

Reverse Words (Keep Word Order Reversed, Not Characters)

// JavaScript
"hello world foo".split(" ").reverse().join(" ")
// "foo world hello"

// Python
" ".join("hello world foo".split()[::-1])
// "foo world hello"

Reverse Each Word but Keep Word Order

// JavaScript
"hello world".split(" ").map(w => reverseString(w)).join(" ")
// "olleh dlrow"

// Python
" ".join(w[::-1] for w in "hello world".split())
// "olleh dlrow"

Check if a String is a Palindrome

A palindrome reads the same forwards and backwards (ignoring case and spaces):

// JavaScript
function isPalindrome(s) {
  const clean = s.toLowerCase().replace(/[^a-z0-9]/g, "");
  const graphemes = [...new Intl.Segmenter().segment(clean)].map(g => g.segment);
  return graphemes.join("") === graphemes.reverse().join("");
}

isPalindrome("racecar")       // true
isPalindrome("A man a plan a canal Panama")  // true
isPalindrome("hello")         // false
# Python
def is_palindrome(s: str) -> bool:
    clean = "".join(c.lower() for c in s if c.isalnum())
    return clean == clean[::-1]

print(is_palindrome("racecar"))   # True
print(is_palindrome("hello"))     # False

Quick Reference

Task JavaScript Python Go PHP
Reverse ASCII s.split("").reverse().join("") s[::-1] []rune(s) swap strrev($s)
Reverse by code point Intl.Segmenter (works for BMP) s[::-1] []rune(s) swap mb_str_split + array_reverse
Reverse grapheme clusters Intl.Segmenter grapheme package uniseg package \X regex split
Reverse words .split(" ").reverse().join(" ") " ".join(s.split()[::-1]) strings.Fields + reverse explode + array_reverse
Palindrome check clean + compare clean + [::-1] rune slice compare strrev compare

Common Pitfalls

1. Using split("") in JavaScript for non-ASCII
"😀".split("").length is 2 in JavaScript — the emoji is split into two surrogate halves. Use [...str] (spread) which iterates by code point, or Intl.Segmenter for grapheme safety.

2. PHP strrev() is byte-aware only
strrev("café") corrupts multi-byte characters. Always use mb_str_split + array_reverse for UTF-8 strings.

3. Go strings are bytes, not runes
Iterating for i, b := range str (wrong: str[i]) gives bytes. Use for _, r := range str to iterate runes, or convert to []rune first.

4. NFC vs NFD matters for reversal
If é is stored as NFD (two code points), reversing by grapheme cluster gives correct output. Reversing by code point gives wrong output because the combining accent detaches from its base letter. Normalize to NFC first: s.normalize("NFC") in JS, unicodedata.normalize("NFC", s) in Python.

5. Palindrome checks must normalize and strip
"Was it a car or a cat I saw?" is a palindrome, but only if you strip spaces and punctuation and lower-case everything first. Always clean before comparing.

6. Reversing RTL text (Arabic, Hebrew)
Right-to-left scripts should not be reversed character-by-character for display — the browser handles directionality via dir="rtl". Reversing Arabic text programmatically is usually wrong unless you specifically need the raw code-point order reversed for an algorithm.


Frequently Asked Questions

Why does "😀".split("").reverse().join("") work but "👨‍👩‍👧".split("").reverse().join("") break?
😀 is a single surrogate pair — reversing two items puts them back in the same order by accident. 👨‍👩‍👧 is five code points joined by Zero Width Joiner characters; reversing them destroys the joining sequence.

Is there a performance cost to grapheme-aware reversal?
Yes — Intl.Segmenter and similar libraries have higher overhead than a simple split(""). For tight loops, normalize to NFC first and use code-point-level reversal if you are certain your data has no ZWJ sequences.

How do I reverse a string in-place without extra memory?
In languages with mutable strings (Go, C), convert to a slice, then swap from both ends toward the middle in a single pass: O(n/2) swaps, O(1) extra memory. In Python and JavaScript, strings are immutable — you always create a new string.

What is a palindrome number?
Convert the integer to a string, then check if the string equals its reverse. Beware: negative numbers (-121) are never palindromes, and the number 0 is always a palindrome.

Can I reverse a string with a stack?
Yes — push each character onto a stack, then pop them off. The LIFO order gives you the reversed string. This is O(n) time and O(n) space, identical to the array-reverse approach. It is useful as a teaching example, not a practical choice.

How do I reverse only a substring?
Slice the portion you want to reverse, apply reversal, then reconstruct: str.slice(0, start) + reverseString(str.slice(start, end)) + str.slice(end).

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools