JavaScript String Methods
Strings are one of the most common data types you’ll work with in JavaScript, and the language gives you a large toolbox of built-in string methods for searching, slicing, transforming, and formatting text. This lesson walks through the full toolbox — how strings actually work under the hood, the most important methods with worked examples, and the mistakes that trip up even experienced developers.
Overview / How String Methods Work
In JavaScript, a string is a primitive value — not an object. Primitives are immutable, which means once a string is created, its contents can never be changed in place. Every method you call on a string, like toUpperCase() or slice(), does not modify the original string. Instead, it reads the original and returns a brand-new string with the result.
So how can a primitive have methods at all, if only objects have properties and methods? This is where JavaScript performs a clever trick called autoboxing. When you write 'hello'.toUpperCase(), the engine temporarily wraps the primitive 'hello' in a hidden String wrapper object, looks up toUpperCase on that wrapper’s prototype (String.prototype), calls it, and then discards the wrapper object. The primitive itself is never touched. This happens so fast and so transparently that most developers never think about it, but it explains why typeof 'hello' is 'string' while still allowing you to call dozens of methods on it.
Because strings are immutable, string methods follow a consistent pattern: they read data from the string and return a new value (usually a new string, but sometimes a number, a boolean, or an array). Nothing about calling a string method ever changes the variable you called it on — you must always capture the return value if you want to keep it, usually by reassigning it to a variable.
Internally, JavaScript strings are sequences of UTF-16 code units. Most everyday characters (Latin letters, digits, common punctuation) occupy a single 16-bit code unit, so indexing and .length behave exactly as you’d expect. Characters outside the Basic Multilingual Plane — many emoji, for example — are represented as a surrogate pair of two code units, which can make .length and bracket indexing behave in surprising ways for those characters. This lesson focuses on standard text; just be aware that emoji and rare scripts are the exception, not the rule.
Syntax
The general form of calling a string method is always the same: a string value (a literal, a variable, or an expression that evaluates to a string), followed by a dot, followed by the method name and its arguments.
stringValue.methodName(argument1, argument2, ...)
Because every method returns a value, calls can be chained directly onto one another: ' Hi '.trim().toLowerCase(). The table below summarizes the most useful string methods you’ll reach for constantly.
| Method | Purpose | Returns |
|---|---|---|
length |
Number of UTF-16 code units (a property, not a method) | number |
charAt(i) / at(i) |
Character at index i (at supports negative indices) |
string |
indexOf(sub) / lastIndexOf(sub) |
Position of first/last match, or -1 |
number |
includes(sub) |
Whether the substring exists | boolean |
startsWith(sub) / endsWith(sub) |
Checks the start/end of the string | boolean |
slice(start, end) |
Extracts a section (supports negative indices) | string |
substring(start, end) |
Extracts a section (negative indices clamp to 0) | string |
split(separator) |
Breaks a string into an array of pieces | array |
replace(a, b) / replaceAll(a, b) |
Swaps matches (first, or all) with new text | string |
trim() / trimStart() / trimEnd() |
Removes whitespace | string |
toUpperCase() / toLowerCase() |
Changes letter case | string |
padStart(len, pad) / padEnd(len, pad) |
Pads to a target length | string |
repeat(n) |
Repeats the string n times |
string |
concat(...strs) |
Joins strings (prefer + or template literals) |
string |
Examples
Example 1: A quick tour of the basics
const name = ' Ada Lovelace ';
console.log(name.length);
console.log(name.trim());
console.log(name.toUpperCase());
console.log(name.trim().toLowerCase());
console.log(name.includes('Lovelace'));
console.log(name.trim().slice(0, 3));
Output:
16
Ada Lovelace
ADA LOVELACE
ada lovelace
true
Ada
Notice that name itself never changes — .length counts every character including the surrounding spaces, .toUpperCase() uppercases letters but leaves whitespace untouched, and chaining .trim().slice(0, 3) works because each method hands its result to the next one in the chain.
Example 2: Searching, replacing, and splitting
const sentence = 'The quick brown fox jumps over the lazy dog';
console.log(sentence.indexOf('fox'));
console.log(sentence.replace('lazy', 'energetic'));
console.log(sentence.replaceAll('o', '0'));
const words = sentence.split(' ');
console.log(words.length);
console.log(words[3]);
console.log(words.join('-'));
Output:
16
The quick brown fox jumps over the energetic dog
The quick br0wn f0x jumps 0ver the lazy d0g
9
fox
The-quick-brown-fox-jumps-over-the-lazy-dog
indexOf('fox') returns 16 because that’s where the substring starts counting from zero. replace() only swaps the first match, while replaceAll() swaps every occurrence of 'o' in the sentence. split(' ') turns the sentence into an array of words, and join('-') does the reverse, gluing the array back into a single string with a different separator.
Example 3: Building formatted output with padding
const items = ['Coffee', 'Tea', 'Sandwich'];
const prices = [3.5, 2, 7.25];
items.forEach((item, i) => {
const price = `$${prices[i].toFixed(2)}`;
console.log(item.padEnd(10, '.') + price.padStart(6));
});
console.log('SKU'.padStart(6) + ' | ' + 'Qty'.padEnd(5) + '|');
console.log('-'.repeat(15));
Output:
Coffee.... $3.50
Tea....... $2.00
Sandwich.. $7.25
SKU | Qty |
---------------
This is a realistic use of string methods: padEnd fills each item name out to a fixed width with dots so prices line up, padStart right-aligns each price, and repeat draws a simple separator line. This kind of formatting shows up constantly in console tools, receipts, and plain-text reports.
How It Works Step by Step (Under the Hood)
Because strings are immutable, a chain of string methods is really a pipeline of brand-new strings, each one thrown away except for the last. Watching each intermediate step makes this concrete:
const raw = ' Hello, World! ';
const step1 = raw.trim();
const step2 = step1.toLowerCase();
const step3 = step2.replace(',', '');
console.log(raw);
console.log(step1);
console.log(step2);
console.log(step3);
Output:
Hello, World!
Hello, World!
hello, world!
hello world!
Each line creates a completely separate string in memory: raw is untouched from start to finish. trim() allocates a new string without the leading/trailing whitespace; toLowerCase() allocates yet another new string from that result; replace() allocates one more. Under the hood, the JavaScript engine (V8, SpiderMonkey, etc.) optimizes this heavily — short strings are often stored inline, and engines use structures like “rope” strings or string interning to avoid copying character data unnecessarily — but conceptually, you should always think of a string method call as “read the input, produce a new output,” never as “mutate this string.”
Common Mistakes
Mistake 1: Treating a truthy/falsy check on indexOf() like a boolean
indexOf() returns a number, not a boolean — and a very common bug is using it directly inside an if statement. When the match happens to be at index 0, the check silently fails, because 0 is falsy:
const text = 'fun times ahead';
if (text.indexOf('fun')) {
console.log('Found "fun"');
} else {
console.log('Not found');
}
Output:
Not found
Even though 'fun' is clearly in the string, this prints “Not found”, because indexOf('fun') is 0 and if (0) is falsy. The fix is to use includes(), which returns a real boolean, or to explicitly compare against -1:
const text = 'fun times ahead';
if (text.includes('fun')) {
console.log('Found "fun"');
} else {
console.log('Not found');
}
Output:
Found "fun"
Mistake 2: Trying to mutate a string with bracket notation
Because array-like bracket access works for reading ('hello'[0] gives 'h'), beginners often assume they can assign through it too, writing something like greeting[0] = 'H'. This does nothing in non-strict mode (the assignment is silently ignored) and throws a TypeError in strict mode (‘use strict’), because string index properties are read-only. Strings simply cannot be modified in place — you must build a new string instead:
const greeting = 'hello';
const capitalized = 'H' + greeting.slice(1);
console.log(capitalized);
console.log(greeting);
Output:
Hello
hello
slice(1) grabs everything from index 1 onward ('ello'), and concatenating it with a capital 'H' produces the transformed string — while the original greeting is completely unaffected, exactly as immutability guarantees.
Best Practices
- Prefer
includes(),startsWith(), andendsWith()over rawindexOf()comparisons — they read clearly and avoid the falsy-zero trap. - Use template literals (
`Hello, ${name}!`) instead of string concatenation with+for readability and easier interpolation of expressions. - Prefer
slice()oversubstring()for extracting substrings —slice()supports negative indices (counting from the end), which is far more predictable thansubstring()‘s index-swapping behavior. - Use
replaceAll()when you need every match replaced; reach forreplace()with a global regular expression only when you need pattern matching beyond a literal substring. - Always reassign the result of a string method — since strings are immutable, `str.trim()` on its own line does nothing useful.
- Use
padStart()/padEnd()for formatting fixed-width output (IDs, receipts, tables) instead of manually building spaces with loops. - Watch out for Unicode: don’t assume
.lengthor bracket indexing accurately count “characters” when the string may contain emoji or other characters outside the Basic Multilingual Plane.
Practice Exercises
- Exercise 1: Write a function
initials(fullName)that takes a string like'grace hopper'and returns'GH'— the uppercase first letter of each word. (Hint: combinesplit(),map(), andtoUpperCase().) - Exercise 2: Write a function
isPalindrome(str)that returnstrueif a string reads the same forwards and backwards, ignoring case and spaces (e.g.'Race car'should returntrue). Think about how to reverse a string usingsplit(''),reverse(), andjoin(''). - Exercise 3: Write a function
maskEmail(email)that takes'alex@example.com'and returns'a***@example.com', keeping the first character of the local part and masking the rest before the@. Expected output formaskEmail('alex@example.com')is'a***@example.com'.
Summary
- Strings are immutable primitives; every string method returns a new value instead of changing the original.
- Autoboxing is what lets a primitive string temporarily act like an object so methods can be called on it.
includes(),startsWith(), andendsWith()return real booleans and are safer than rawindexOf()checks.slice(),split(),replace()/replaceAll(), and template literals cover the vast majority of everyday string work.padStart(),padEnd(), andrepeat()are the go-to tools for formatting fixed-width text output.- Always capture the return value of a string method — the original variable is never modified in place.
