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. π
at, includes, indexOf, startsWith, match.
slice vs substring, split and join.
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
| Method | What it does | Example | Result |
|---|---|---|---|
| length | Number of UTF-16 code units | "hello".length | 5 |
| s[i] | Character at index | "hello"[1] | "e" |
| s[i] out of range | Returns 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.fromCodePoint | Code point β string | String.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.
03 Β· Find things
π Search: includes, indexOf, startsWith, match
| Method | What it does | Example | Result |
|---|---|---|---|
| includes | Substring present? (case-sensitive) | "Hello".includes("ell") | true |
| startsWith | Prefix check, optional start position | "report.pdf".startsWith("rep") | true |
| endsWith | Suffix check | "report.pdf".endsWith(".pdf") | true |
| indexOf | First position, or -1 | "hello world".indexOf("o") | 4 |
| indexOf(v, from) | Search from a position | "hello world".indexOf("o", 5) | 7 |
| lastIndexOf | Last position, or -1 | "report.final.pdf".lastIndexOf(".") | 12 |
| search | Index of first regex match | "abc123".search(/\d/) | 3 |
| match | First match + groups (no g) | "ORD-2026".match(/(\d+)/)[1] | "2026" |
| match /g | All matches as an array, or null | "a1b22c333".match(/\d+/g) | ["1", "22", "333"] |
| matchAll | Iterator of every match with groups (needs g) | [..."a=1,b=2".matchAll(/(\w)=(\d)/g)].map(m => m[1]) | ["a", "b"] |
Case-insensitive contains: there's no flag on includes. Use s.toLowerCase().includes(q.toLowerCase()) for simple cases, or a regex with the i flag β and escape user input before putting it in a regex. Test patterns in the Regex Tester.
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" | Result | Why |
|---|---|---|
| .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
| Example | Result | Note |
|---|---|---|
| "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 ",". |
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.
| Example | Result |
|---|---|
| "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
| Pattern | Inserts |
|---|---|
| $& | 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.
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
| Method | Example | Result |
|---|---|---|
| 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().length | 4 |
| normalize("NFD") | "Γ©".normalize("NFD").length | 2 |
| 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:
["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.
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:
const escapeHtml = s => String(s) .replaceAll("&", "&").replaceAll("<", "<") .replaceAll(">", ">").replaceAll('"', """); 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 <script></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
-
π
replacechanges only the first matchWith a string pattern, or a regex without
g, only the first occurrence is replaced. UsereplaceAllor add thegflag. Code reviews miss this constantly because it passes a test with one occurrence. -
π
length, indexes and emojiAll index-based methods β
length,slice,charAt,split("")β work on UTF-16 units. Truncating withs.slice(0, 10)can cut an emoji in half and leave a lone surrogate. Iterate withfor...ofor[...s]for code points, andIntl.Segmenterfor visible characters.s.isWellFormed()(ES2024) tells you if a lone surrogate slipped through. -
π§ Immutability β even with index assignment
s[0] = "X"does nothing: it's silently ignored in sloppy mode and throws aTypeErrorin strict mode (including ES modules and classes). Build a new string:s = "X" + s.slice(1). -
βοΈ
substringswaps and clampsNegative arguments become 0 and reversed arguments are swapped, so bad index maths returns a plausible-looking wrong string instead of failing.
sliceis more predictable. -
πͺ
splitwith a limit drops data"a,b,c".split(",", 2)returns["a", "b"]β"c"is gone. Unlike C#'sSplit(',', 2), the remainder is not kept. UseindexOf+sliceto split once. -
π¦
new String()is an objecttypeof new String("a")is"object", andnew String("a") === "a"isfalse. UseString(value)(nonew) to convert β it also handlesnullandundefined, wherevalue.toString()throws. -
π€ Unicode-lookalike strings
"caf\u00E9" === "cafe\u0301"isfalseeven 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.