Skip to content
Browse tools

Practical reference Β· JavaScript strings

The string methods
you reach for, and
the traps in each.

JavaScript strings look simple until an emoji makes .length lie, replace only changes the first match, or substring quietly swaps your arguments. This page walks through the methods by job, with the exact output of every example, and ends with the gotchas worth memorising. πŸ‘‡

πŸ”
Read
Access & search

at, includes, indexOf, startsWith, match.

βœ‚οΈ
Cut
Slice & split

slice vs substring, split and join.

πŸ› οΈ
Change
Replace & format

replaceAll, regex, padding, case, template literals.

🧊

Strings are immutable primitives. Every method returns a new string; none changes the one you called it on. name.trim() on its own line does nothing β€” name = name.trim() does.

02 Β· Look inside

πŸ” Length and character access

MethodWhat it doesExampleResult
lengthNumber of UTF-16 code units"hello".length5
s[i]Character at index"hello"[1]"e"
s[i] out of rangeReturns undefined"hello"[10]undefined
charAt(i)Like s[i], but "" when out of range"hello".charAt(10)""
at(i)Supports negative indexes (ES2022)"hello".at(-1)"o"
charCodeAt(i)UTF-16 unit as a number"A".charCodeAt(0)65
codePointAt(i)Full Unicode code point"πŸ˜€".codePointAt(0)128512
String.fromCodePointCode point β†’ stringString.fromCodePoint(128512)"πŸ˜€"
[...s]Array of code points (emoji-safe for simple emoji)[..."aπŸ˜€"]["a", "πŸ˜€"]
πŸ˜€

length counts UTF-16 units, not characters. "πŸ˜€".length is 2, "πŸ‘πŸ½".length is 4, and "πŸ‘¨β€πŸ‘©β€πŸ‘§".length is 8. To count what a user sees, use [...new Intl.Segmenter().segment(s)].length.

04 Β· Cut it up

βœ‚οΈ slice vs substring, split and join

Three methods take a part of a string. Use slice; know why the others surprise people.

Call on "JavaScript"ResultWhy
.slice(4)"Script"From index 4 to the end.
.slice(0, 4)"Java"End index is exclusive.
.slice(-3)"ipt"Negative counts from the end.
.slice(4, -2)"Scri"Same as slice(4, 8).
.substring(4, -2)"Java"Negative becomes 0, then the arguments are swapped β†’ substring(0, 4).
.substring(4, 0)"Java"Swaps start and end if start > end.
.substr(4, 3)"Scr"Start + length. Legacy (Annex B) β€” avoid in new code.

πŸͺ“ split and join

ExampleResultNote
"a,b,,c".split(",")["a", "b", "", "c"]Empty entries are kept.
"a,b,,c".split(",").filter(Boolean)["a", "b", "c"]Drop empty entries.
" a , b ".split(",").map(s => s.trim())["a", "b"]Trim each piece.
"a, b;c".split(/[,;]\s*/)["a", "b", "c"]Split on a regex.
"a,b,c".split(",", 2)["a", "b"]The limit discards the rest β€” it doesn't keep it in the last piece.
"line1\r\nline2".split(/\r?\n/)["line1", "line2"]Handles Windows and Unix line endings.
"hi".split("")["h", "i"]Splits into UTF-16 units β€” breaks emoji. Use [...s].
["a", "b", "c"].join(" | ")"a | b | c"Default separator is ",".
β–Έ key=value where the value may contain "="
const line = "token=abc=def";
const i = line.indexOf("=");
const key = line.slice(0, i);          // "token"
const value = line.slice(i + 1);       // "abc=def"

05 Β· Swap text

πŸ” replace, replaceAll and regex

The single most common JavaScript string bug: replace with a string pattern replaces only the first match.

ExampleResult
"a-b-c".replace("-", "+")"a+b-c" (first only!)
"a-b-c".replaceAll("-", "+")"a+b+c"
"a-b-c".replace(/-/g, "+")"a+b+c"
"Hi HI hi".replace(/hi/gi, "yo")"yo yo yo"
"a b c".replace(/\s+/g, " ")"a b c"
"John Smith".replace(/(\w+) (\w+)/, "$2, $1")"Smith, John"
"2026-10-01".replace(/(?<y>\d+)-(?<m>\d+)-(?<d>\d+)/, "$<d>/$<m>/$<y>")"01/10/2026"
"price: 5".replace(/\d+/, n => n * 2)"price: 10"
"a-b".replaceAll(/-/, "+")TypeError β€” regex needs g

πŸ’² Special patterns in the replacement string

PatternInserts
$&The whole match
$1, $2 …Numbered capture groups
$<name>Named capture group
$` / $'Text before / after the match
$$A literal $
πŸ’²

These patterns apply even with a string search. "cost: X".replace("X", "$5") works, but a replacement containing $& or $$ is interpreted. If the replacement is user input, pass a function instead β€” s.replace(find, () => userText) inserts it literally.

β–Έ escape user input before building a regex
const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

const term = "1+1";
const re = new RegExp(escapeRegExp(term), "gi");
"1+1=2, 1+1 again".replace(re, "two");   // "two=2, two again"

πŸ’‘ Doing a one-off bulk replacement on a document? Find & Replace supports both plain text and regex, and shows the result before you copy it.

06 Β· Shape it

πŸ› οΈ Trim, pad, repeat, case and normalize

MethodExampleResult
trim" hi ".trim()"hi"
trimStart / trimEnd" hi ".trimEnd()" hi"
padStart"5".padStart(3, "0")"005"
padEnd"abc".padEnd(6, ".")"abc..."
padStart (already long)"12345".padStart(3, "0")"12345" (never truncates)
repeat"ab".repeat(3)"ababab"
toUpperCase"Hello".toUpperCase()"HELLO"
toLowerCase"Order-ID".toLowerCase()"order-id"
toUpperCase (ß)"straße".toUpperCase()"STRASSE" (longer!)
toLocaleUpperCase"i".toLocaleUpperCase("tr")"Δ°"
normalize"cafe\u0301".normalize().length4
normalize("NFD")"Γ©".normalize("NFD").length2
concat"a".concat("b", "c")"abc"

πŸ”  Comparing and sorting: localeCompare

The < and > operators compare UTF-16 code units, so "Z" < "a" is true and "Γ©" sorts after "z". For anything a person reads, use localeCompare or Intl.Collator:

β–Έ JavaScript
["b", "A", "a", "B"].sort()                           // ["A", "B", "a", "b"]
["b", "A", "a", "B"].sort((x, y) => x.localeCompare(y))  // ["a", "A", "b", "B"] (en)

// natural sort: item2 before item10
const natural = new Intl.Collator(undefined, { numeric: true });
["item10", "item2"].sort(natural.compare)                // ["item2", "item10"]

// case- and accent-insensitive equality
"resume".localeCompare("RΓ©sumΓ©", undefined, { sensitivity: "base" }) === 0   // true

πŸ’‘ When sorting large arrays, create one Intl.Collator and reuse its compare β€” it's faster than calling localeCompare with options on every comparison.

07 Β· Build strings

🧾 Template literals and String.raw

Backtick strings allow embedded expressions and real line breaks. They're the default way to build strings in modern JavaScript.

β–Έ JavaScript
const orderId = "ORD-2026-000873";
const total = 1234.5;

`Order ${orderId} is ready.`
// "Order ORD-2026-000873 is ready."

`Total: ${total.toFixed(2)}`                   // "Total: 1234.50"
`Total: ${total.toLocaleString("en-US", { style: "currency", currency: "USD" })}`
// "Total: $1,234.50"

// multi-line β€” the line breaks and indentation are part of the string
const sql = `SELECT id, name
FROM customers
WHERE id = ?`;

// String.raw: backslashes stay literal
String.raw`C:\temp\new`                        // "C:\temp\new" (no tab, no newline)
String.raw`\d+`.length                           // 3

🏷️ Tagged templates

A function placed before a template literal receives the literal parts and the values separately. That's how libraries build safe SQL, escaped HTML or styled components β€” the tag decides how each value is inserted:

β–Έ JavaScript
const escapeHtml = s => String(s)
  .replaceAll("&", "&amp;").replaceAll("<", "&lt;")
  .replaceAll(">", "&gt;").replaceAll('"', "&quot;");

function html(strings, ...values) {
  return strings.reduce((out, str, i) =>
    out + str + (i < values.length ? escapeHtml(values[i]) : ""), "");
}

const name = "<script>";
html`<p>Hello ${name}</p>`;   // "<p>Hello &lt;script&gt;</p>"
πŸ”—

Building URLs? Don't interpolate raw values into query strings. Use encodeURIComponent(value) for each value, or new URLSearchParams({ q: value }), which handles it for you. The URL Encoder / Decoder shows the expected output.

08 Β· Read before you ship

πŸͺ€ Gotchas that cause real bugs

  1. πŸ” replace changes only the first match

    With a string pattern, or a regex without g, only the first occurrence is replaced. Use replaceAll or add the g flag. Code reviews miss this constantly because it passes a test with one occurrence.

  2. πŸ˜€ length, indexes and emoji

    All index-based methods β€” length, slice, charAt, split("") β€” work on UTF-16 units. Truncating with s.slice(0, 10) can cut an emoji in half and leave a lone surrogate. Iterate with for...of or [...s] for code points, and Intl.Segmenter for visible characters. s.isWellFormed() (ES2024) tells you if a lone surrogate slipped through.

  3. 🧊 Immutability β€” even with index assignment

    s[0] = "X" does nothing: it's silently ignored in sloppy mode and throws a TypeError in strict mode (including ES modules and classes). Build a new string: s = "X" + s.slice(1).

  4. βœ‚οΈ substring swaps and clamps

    Negative arguments become 0 and reversed arguments are swapped, so bad index maths returns a plausible-looking wrong string instead of failing. slice is more predictable.

  5. πŸͺ“ split with a limit drops data

    "a,b,c".split(",", 2) returns ["a", "b"] β€” "c" is gone. Unlike C#'s Split(',', 2), the remainder is not kept. Use indexOf + slice to split once.

  6. πŸ“¦ new String() is an object

    typeof new String("a") is "object", and new String("a") === "a" is false. Use String(value) (no new) to convert β€” it also handles null and undefined, where value.toString() throws.

  7. πŸ”€ Unicode-lookalike strings

    "caf\u00E9" === "cafe\u0301" is false even though both display as cafΓ©. Call .normalize() on user input before comparing, storing or deduplicating it.

🏁

Three habits cover most of it: use replaceAll (or /g), use slice instead of substring, and never trust length for user-visible characters. Try transformations in String Tools, read What are string utilities? for the Unicode background, or compare with C# string methods.

πŸ“Œ Behaviour described follows the ECMAScript specification as implemented in current browsers and Node.js. at, replaceAll and Intl.Segmenter need a reasonably modern runtime; isWellFormed is ES2024. The reference is MDN's String documentation.

JavaScript string methods
9 min read