String Methods
The built-in tools for reading, searching, and reshaping text.
What is it?
Text shows up everywhere in a program — a name, a message, a URL — and you constantly need to answer small questions about it: how long is it? does it contain this word? what does it look like in uppercase? JavaScript strings come with a large set of built-in methods that answer exactly these kinds of questions, so you rarely need to process text character-by-character yourself.
A few you'll reach for constantly: length (a property, not a method, but just as essential), slice, includes, split, trim, replace, and template literals for building strings out of pieces.
Explain like I'm 10
If a string is a train of connected train cars, string methods are the tools a train inspector carries — one to count the cars (length), one to pull out a section (slice), one to check if a car of a certain type exists (includes), one to uncouple the whole train into individual cars (split).
Examples
Common string operations
const message = " Hello, World! ";
console.log(message.length); // 18 (includes the spaces)
console.log(message.trim()); // "Hello, World!"
console.log(message.includes("World")); // true
console.log(message.toLowerCase()); // " hello, world! "
const name = "Amara";
console.log(`Hi, ${name}!`); // "Hi, Amara!" — a template literal
const parts = "2026-08-29".split("-"); // ["2026", "08", "29"]Searching and replacing within a string
const sentence = "The cat sat on the mat";
console.log(sentence.indexOf("cat")); // 4
console.log(sentence.replace("cat", "dog")); // "The dog sat on the mat"
console.log(sentence.startsWith("The")); // true
console.log(sentence.slice(4, 7)); // "cat"indexOf finds a position, replace swaps text, startsWith checks the beginning, and slice pulls out a substring by position — all without touching the original string.
How it works
Because strings are immutable in JavaScript, every one of these methods returns a brand-new string (or array) rather than modifying the original — trim() doesn't change message, it hands you back a new, trimmed copy. length is different: it's a property, not a method, so you read it without parentheses.
Why does it exist?
Handling text by hand — walking character by character to search or reshape it — is slow to write and easy to get wrong. Built-in string methods cover the overwhelming majority of everyday text tasks with one clear, well-tested call.
When to use it
Reach for these whenever you're validating, searching, formatting, or reshaping text — checking a username's length, confirming an email contains "@", splitting a comma-separated list, or building a message from variables with a template literal.
When not to use it
For very heavy text processing — parsing a complex format, matching flexible patterns — plain string methods can get unwieldy; that's when a regular expression is worth reaching for instead. And building a large string through many small concatenations in a loop is slower than assembling an array of pieces and joining it once.
Common mistakes
Forgetting that string methods don't mutate —
str.trim()does nothing unless you use or store its return value.Using
+to build long strings piece by piece inside a loop instead of a template literal or joining an array.Forgetting that
.lengthcounts UTF-16 code units, which can differ from the number of visible characters for some emoji and special characters.
Practice exercises
- Easy:
Use
.toUpperCase()and.lengthto print a name in all caps along with its length. - Medium:
Use
.split()and.join()to reverse the order of words in a sentence. - Hard:
Write a function that checks whether a string is a valid-looking email (contains exactly one "@" and at least one "." after it) using only string methods.
Interview questions
Why do string methods like `trim()` or `toUpperCase()` never modify the original string?
Strings are immutable in JavaScript — every string method returns a brand-new string; there's no way to change the characters of an existing string value in place.
What does this log? `let str = "hello"; str.toUpperCase(); console.log(str);`
"hello", unchanged — toUpperCase() returns a new uppercase string but doesn't alter str itself; the return value is thrown away because it was never assigned back, e.g. str = str.toUpperCase();.
What's the difference between `slice()` and `substring()` on a string?
Both extract a range, but slice() accepts negative indexes to count from the end of the string, while substring() treats a negative argument as 0 and automatically swaps the two arguments if the start is greater than the end.
What does this log? `const s = "Hello, World!"; console.log(s.slice(-6));`
"World!" — a negative index in slice() counts backward from the end of the string, so -6 starts six characters before the end.
Are string comparisons with `includes()`, `startsWith()`, and `endsWith()` case-sensitive?
Yes, all of them compare exact character codes — "Hello".includes("hello") is false. A case-insensitive check needs an explicit .toLowerCase() (or .toUpperCase()) on both sides first.
What does this log? `console.log("Hello".includes("hello"));`
false — string methods compare characters exactly as written; "H" and "h" are different code points, so nothing about includes() normalizes case automatically.
What can a template literal do that regular quoted strings can't?
It can span multiple lines without escape characters, and it can embed any JavaScript expression directly inside a ${...} placeholder, which gets evaluated and converted to a string — replacing manual concatenation with +.
What does `String(null)` return, compared to `null + ""`?
Both produce "null" — explicit String() conversion and implicit coercion via + follow the same underlying string-conversion rules for primitives, unlike JSON.stringify(null), which also returns "null" but for the unrelated reason of serializing the JSON value null.
What is a tagged template literal?
A function call written directly in front of a template literal — instead of interpolating automatically, the tag function receives the literal string pieces and the interpolated values as separate arguments, letting it process or escape them however it wants before building the final string.
What does this log, and why? `console.log("2" > "10");`
true. Comparing strings with >/< is lexicographic (character by character, by Unicode code point), not numeric — the first character "2" is greater than "1", so the comparison stops there and returns true, regardless of what follows.
What does this log? `console.log(1 + "1"); console.log("1" + 1); console.log(1 + 1 + "1"); console.log("1" + 1 + 1);`
"11", "11", "21", "111". + evaluates left to right; the moment either operand of a given + is a string, that operation becomes concatenation. 1 + 1 + "1" adds the two numbers first (2), then concatenates with "1"; "1" + 1 + 1 concatenates immediately at the first +, so every following + is string concatenation too.
What do `padStart()` and `padEnd()` do?
They pad a string to a target length by adding a given fill string at the start or end (repeated as needed) until it reaches that length — commonly used to zero-pad numbers or align text in columns.
What does this log? `console.log("5".padStart(3, "0"));`
"005" — padStart adds copies of "0" to the front until the string reaches a total length of 3.
Why might `.length` not match the number of visible characters in a string?
.length counts UTF-16 code units, not visible characters (grapheme clusters). Characters outside the Basic Multilingual Plane — many emoji included — are represented as a pair of code units (a surrogate pair), so they count as 2 even though they display as one character.
What does `console.log("😀".length);` log, and why isn't it `1`?
2 — that emoji falls outside the Basic Multilingual Plane and is stored internally as a UTF-16 surrogate pair (two 16-bit code units), and .length counts code units, not the single visual character they represent together.
What's the difference between `trim()`, `trimStart()`, and `trimEnd()`?
trim() removes whitespace from both ends of a string; trimStart() removes it only from the beginning, and trimEnd() only from the end.
What does this log? `console.log("abc".split(""));`
["a", "b", "c"] — splitting on an empty string separator breaks the string apart between every character, producing an array of individual characters.
What's the difference between `replace()` and `replaceAll()`?
replace(search, value) with a plain string only replaces the first match; to replace every occurrence with replace, you'd need a regex with the global flag. replaceAll() replaces every occurrence directly, even with a plain string search term.
What does this log? `console.log("cat cat cat".replace("cat", "dog"));`
"dog cat cat" — with a plain string (not a global regex), replace() stops after the first match and leaves the rest of the string untouched.
Why is repeatedly concatenating strings with `+=` inside a large loop considered inefficient?
Because strings are immutable, every += creates an entirely new string and copies all the previous characters into it — for n concatenations that's roughly O(n squared) total copying. Pushing pieces into an array and calling .join("") once at the end avoids the repeated copying.
Why can you safely compare two string primitives with `===`, unlike two objects?
Strings are primitive values compared by their actual content, not by reference — two variables holding the identical sequence of characters are ===, whereas two objects (even with identical contents) are only === if they're literally the same object in memory.
What does this log? `const a = "hi"; const b = "hi"; console.log(a === b);`
true — string literals with the same characters are equal by value, since strings are primitives, not reference types like arrays or objects.
What's the difference between `charAt(i)`, `str[i]`, and `str.at(i)` for reading a character?
All three read the character at index i for a valid index. Out of range, charAt returns an empty string "", while bracket notation and .at() return undefined; .at() additionally supports negative indexes to count from the end, which neither of the other two does.
How would you check if a string is a palindrome using only built-in string and array methods?
Split it into characters (str.split("")), reverse that array (.reverse()), join it back into a string (.join("")), and compare the result to the original — strings don't have their own .reverse(), so this detour through an array is the standard approach.
What does `split(separator, limit)`'s second argument do?
It caps how many pieces the resulting array can contain — the string is still split at every occurrence of the separator, but the array is truncated to at most limit entries, discarding anything past that.
Can `split()` take a regular expression as its separator?
Yes — splitting on a regex lets you break a string apart on a pattern rather than a fixed substring, for example splitting on any run of whitespace with str.split(/\s+/).
Why is the risk with using `+` to build a dynamic string versus a template literal more than just readability?
Beyond being harder to read with many pieces, + chains are easy to get wrong around operator precedence and type coercion — numbers sliding into unintended string concatenation, as with 1 + 1 + "1" — while a template literal makes every interpolated value's boundary explicit and evaluates each ${...} independently.