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(), and endsWith() over raw indexOf() 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() over substring() for extracting substrings — slice() supports negative indices (counting from the end), which is far more predictable than substring()‘s index-swapping behavior.
  • Use replaceAll() when you need every match replaced; reach for replace() 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 .length or 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: combine split(), map(), and toUpperCase().)
  • Exercise 2: Write a function isPalindrome(str) that returns true if a string reads the same forwards and backwards, ignoring case and spaces (e.g. 'Race car' should return true). Think about how to reverse a string using split(''), reverse(), and join('').
  • 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 for maskEmail('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(), and endsWith() return real booleans and are safer than raw indexOf() checks.
  • slice(), split(), replace()/replaceAll(), and template literals cover the vast majority of everyday string work.
  • padStart(), padEnd(), and repeat() 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.