Problem Solved

JavaScript · string methods

Home › Courses › JavaScript › Problems › String methods
🟨 Concept · 3 min read

String methods.
slice, split, replace, includes.

Strings have 30+ methods. Six do 90% of the work: slice, split, replace, includes, trim, and the case converters. This page covers what each does, the gotchas, and the patterns that come up in real code — like forgetting that replace only replaces the first match, or that strings are immutable so the result must be reassigned.

Short answer: slice(start, end) extracts a portion, supports negative indices. split(sep) breaks into an array. replace(a, b) replaces the first match — use replaceAll or a global regex for all. includes(str) returns a boolean. trim() removes whitespace. Strings are immutable — every method returns a new string, so assign the result. String comparison is case-sensitive and locale-blind — use toLowerCase() or localeCompare.

✅ The six you'll use daily

What each does, in one line.

slice(start, end)

Extracts a substring. Negative indices from the end. 'hello'.slice(-3) → 'llo'.

split(sep)

Breaks into an array. 'a,b,c'.split(',') → ['a','b','c']. Split on '' to get characters.

replace(a, b)

Replaces the first match only. Use replaceAll or /a/g for all. Strings are immutable.

includes(str)

Returns true if the substring exists. Case-sensitive. For case-insensitive, lowercase both sides.

trim()

Removes whitespace from both ends. trimStart() and trimEnd() for one side only.

toUpperCase() / toLowerCase()

Changes case. Used for case-insensitive comparison: a.toLowerCase() === b.toLowerCase().

✅ The methods in action

A realistic string, six operations.

string-methods.js
const str = '  Hello, World!  ';

// slice — extract a portion
console.log(str.slice(2, 7));      // 'Hello'
console.log(str.slice(-3));        // '!  ' (last 3 chars)
console.log(str.slice(2));         // 'Hello, World!  '

// split — break into an array
console.log('a,b,c'.split(','));    // ['a', 'b', 'c']
console.log('hello'.split(''));    // ['h', 'e', 'l', 'l', 'o']
console.log(str.split(' '));        // ['', '', 'Hello,', 'World!', '', '']

// replace — first match only
console.log(str.replace('o', '0'));  // '  Hell0, World!  '
console.log(str.replaceAll('o', '0')); // '  Hell0, W0rld!  '

// includes
console.log(str.includes('World'));  // true
console.log(str.includes('world'));  // false (case-sensitive)

// trim
console.log(str.trim());            // 'Hello, World!'
console.log(str.trimStart());       // 'Hello, World!  '
console.log(str.trimEnd());         // '  Hello, World!'

// case
console.log(str.trim().toUpperCase()); // 'HELLO, WORLD!'
console.log(str.trim().toLowerCase()); // 'hello, world!'
Key details:

slice:
  slice(2, 7) → from index 2 up to (not including) index 7
  slice(-3)   → from index length-3 to the end
  slice(2)    → from index 2 to the end
  Out-of-range indices are clamped, not thrown.

split:
  split(',')  → split on every comma
  split('')   → split into individual characters
  split(' ')  → split on every space (multiple spaces = empty strings)
  If the separator isn't found, returns [wholeString].

replace:
  replace('o', '0')     → first 'o' only
  replace(/o/g, '0')    → all 'o' (regex with g flag)
  replaceAll('o', '0')  → all 'o' (simpler syntax)
  The replacement can be a function: replace('o', () => '0')

includes:
  Case-sensitive by default.
  For case-insensitive:
    str.toLowerCase().includes(sub.toLowerCase())
  For regex, use str.match(/pattern/) or str.search(/pattern/).

trim:
  Removes spaces, tabs, newlines, and Unicode whitespace.
  Non-whitespace characters (including zero-width spaces) are preserved.
  trimStart and trimEnd for one-sided trim.

Case:
  toUpperCase / toLowerCase are locale-insensitive.
  For Turkish (dotless i), use toLocaleUpperCase('tr').
  For most code, the simple versions are fine.

Strings are IMMUTABLE.
Every method returns a new string.
The original is unchanged.

❌ str.toUpperCase();   // result discarded
✅ str = str.toUpperCase();  // result kept

✅ The gotchas

What actually trips people up.

gotchas.js
// ❌ replace only replaces the first match
console.log('a-a-a'.replace('a', 'b'));   // 'b-a-a'

// ✅ Use replaceAll or a global regex
console.log('a-a-a'.replaceAll('a', 'b')); // 'b-b-b'
console.log('a-a-a'.replace(/a/g, 'b'));   // 'b-b-b'

// ❌ Strings are immutable — result must be reassigned
let s = 'hello';
s.toUpperCase();
console.log(s);   // 'hello' (unchanged)

s = s.toUpperCase();
console.log(s);   // 'HELLO'

// ❌ Case-sensitive includes
console.log('Hello'.includes('hello'));  // false

// ✅ Lowercase both sides for case-insensitive
console.log('Hello'.toLowerCase().includes('hello'.toLowerCase()));
// true

// ❌ slice and substring differ on negatives
console.log('hello'.slice(-3));      // 'llo'
console.log('hello'.substring(-3));  // 'hello'

// ✅ slice is the modern default

// ❌ Comparing strings with > or < is code-unit based
console.log('Z' < 'a');   // true (uppercase before lowercase)

// ✅ Use localeCompare for locale-aware comparison
console.log('Z'.localeCompare('a'));  // 1 (Z > a in en locale)
The five gotchas to know:

1. replace only replaces first match.
   For all: replaceAll (ES2021) or /pattern/g.
   The most common string bug in JavaScript.

2. Strings are immutable.
   Every method returns a new string.
   The original is unchanged.
   Always assign: str = str.method().

3. includes is case-sensitive.
   For case-insensitive: toLowerCase() both sides.
   There's no includesIgnoreCase.

4. slice vs substring differ on negatives.
   slice(-3) → last 3 chars.
   substring(-3) → whole string (treats negative as 0).
   Use slice. Ignore substring.

5. String comparison is code-unit based.
   'Z' < 'a' is true (uppercase before lowercase).
   For locale-aware: localeCompare.
   For case-insensitive equality:
     a.toLowerCase() === b.toLowerCase().

Bonus: split on a single space
  'a  b'.split(' ') → ['a', '', 'b']
  Multiple spaces produce empty strings.
  If you want any whitespace:
    str.split(/\s+/)
  This splits on one or more whitespace characters.

Bonus: string.length is UTF-16 code units
  '😀'.length === 2 (surrogate pair)
  For grapheme count: [...str].length
  For characters as user sees them: use Intl.Segmenter

These aren't bugs — they're the spec.
Knowing them saves debugging time.

✅ Chaining

String methods chain naturally.

chaining.js
// Chaining — each returns a new string or value
const result = '  Hello, World!  '
  .trim()                     // 'Hello, World!'
  .toLowerCase()              // 'hello, world!'
  .replace('world', 'JS')     // 'hello, JS!'
  .split(', ')                // ['hello', 'JS!']
  .join(' & ');               // 'hello & JS!'

console.log(result);
// 'hello & JS!'

// Without chaining — same result, more code
let a = '  Hello, World!  ';
a = a.trim();
a = a.toLowerCase();
a = a.replace('world', 'JS');
a = a.split(', ');
a = a.join(' & ');

// Both work. Chaining is more readable for transformations.

// Real-world: format a slug
function slugify(str) {
  return str
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, '')   // remove special chars
    .replace(/\s+/g, '-')        // spaces to hyphens
    .replace(/-+/g, '-');        // collapse multiple hyphens
}

console.log(slugify('Hello, World!'));    // 'hello-world'
console.log(slugify('  My  Post!  '));    // 'my-post'
Why chaining works:

Every string method that "modifies" returns
a new string. So you can call another method
on the result.

  str.trim()        → returns a new string
  .toLowerCase()    → returns a new string
  .replace(...)     → returns a new string
  .split(...)       → returns an array
  .join(...)        → returns a string

The chain reads left to right, top to bottom.

When to chain vs when to break up:

✅ Chain when the transformations are short:
   str.trim().toLowerCase()

✅ Chain when each step is obvious:
   str.trim().toLowerCase().replace(...)

❌ Break when a step needs a comment:
   str
     .trim()
     .toLowerCase()
     // step 3: remove all non-word chars
     .replace(/[^\w\s-]/g, '')

❌ Break when the chain is > 4-5 steps
   — debugging a long chain is hard

String chaining is one of the most-used
patterns in JavaScript. Every URL parser,
config loader, and form handler uses it.

Once you know the methods, the chains
write themselves.

Don't forget that replace only replaces the first match. This is the most common string bug in JavaScript. 'a-a-a'.replace('a', 'b') gives 'b-a-a', not 'b-b-b'. Every developer has shipped this bug at least once. The fix is one of three: replaceAll('a', 'b') (ES2021, all modern environments), replace(/a/g, 'b') (regex with global flag, works everywhere), or split('a').join('b') (works in older environments). If you're replacing a string, be explicit — replace with a string is first-match-only, replaceAll or /g replaces everything.

⚠️ Common mistakes

The string method mistakes that cost real time.

1. replace('a', 'b') for all matches

Only replaces the first. Fix: replaceAll or /a/g.

2. Calling a method and not assigning

Strings are immutable — the original is unchanged. Fix: str = str.method().

3. Expecting includes to be case-insensitive

It's case-sensitive. Fix: toLowerCase() both sides.

4. Using substring with negative indices

Treats negatives as 0. Fix: use slice.

5. Comparing strings with < and >

Code-unit based. Fix: localeCompare for locale-aware.

6. Splitting on a single space

Multiple spaces produce empty strings. Fix: split(/\s+/).

7. Assuming .length is character count

UTF-16 code units — emoji count as 2. Fix: [...str].length for code points.

8. Chaining too long without breaking

Hard to debug. Fix: break when the chain exceeds 4-5 steps.

❓ Frequently asked

What are the most important JavaScript string methods?

The core set: slice, split, replace, includes, trim, toUpperCase, toLowerCase, padStart. Most real-world string work uses five or six of them. The array methods map, filter, reduce are for arrays; for strings, these are the equivalents.

What is the difference between slice and substring?

Both extract part of a string, but they handle negative values differently. slice accepts negative indices — 'hello'.slice(-3) gives 'llo'. substring treats negative indices as 0 — 'hello'.substring(-3) gives 'hello'. The recommendation: use slice. It's more predictable.

How do I replace all occurrences of a string?

Three ways. (1) replace with a global regex: str.replace(/foo/g, 'bar'). (2) replaceAll (ES2021): str.replaceAll('foo', 'bar'). (3) split and join: str.split('foo').join('bar'). The gotcha: replace with a string only replaces the first occurrence.

Why does string comparison fail?

Strings compare by UTF-16 code unit, not by locale. 'Z' < 'a' is true because uppercase letters come before lowercase in ASCII. For locale-aware comparison, use localeCompare. For case-insensitive comparison, lowercase both strings first.

What is the difference between trim and trimStart?

trim() removes whitespace from both ends. trimStart() removes from the beginning only. trimEnd() removes from the end only. All three methods handle the full range of whitespace characters.

Are strings mutable in JavaScript?

No. Strings are immutable — any method that 'modifies' a string actually returns a new one. str.toUpperCase() does not change str; it returns a new string. The consequence: you must assign the result: str = str.toUpperCase().

🔗 Continue learning

🗺️ JavaScript learning path
The full roadmap from variables to async — every concept in order

Next in this series:

📊 Object.keys, values, entries
Iterating over objects — and the difference between the three
← All JavaScript problems
Problem solved · JavaScript