# String methods

Source: https://codewiki.com/javascript/string-methods/

> - **what**: A JavaScript string is an immutable sequence of UTF-16 code units; string methods search, extract, transform, compare, or split that sequence.
> - **trap**: `length`, `slice()`, and most position arguments count UTF-16 code units, not necessarily user-perceived characters; `replace()` also replaces only the first string match by default.
> - **fix**: Decide whether the task needs code units, Unicode code points, or grapheme clusters before choosing a method; use `Intl.Segmenter` for user-visible text and `Intl.Collator` for locale-aware comparison.

## What it is and why it exists

JavaScript strings represent text values. A string primitive is immutable: a method may inspect it or return a transformed string, but it can't rewrite a position in the original. Assignment makes a variable refer to another string; it doesn't mutate the old value.

String methods organize common text operations on `String.prototype`. You use `includes()` for a fixed fragment, `slice()` for a half-open range, `replace()` or `replaceAll()` for substitutions, and `trim()` for boundary whitespace. Case conversion, normalization, and locale-aware comparison methods handle other international text requirements.

These methods solve problems at different layers. A fixed delimiter suits `indexOf()` and `slice()`, a pattern language belongs to regular expressions, and a grammar requires a dedicated parser. `split(',')` isn't a CSV parser, and a regular-expression replacement isn't an HTML sanitizer.

The underlying counting unit directly affects results. JavaScript stores and indexes strings as UTF-16 code units. One Unicode code point may occupy one or two code units, and one symbol perceived by a user may contain several code points.

That makes “take the first ten characters” an incomplete requirement. A protocol field defined in code units can use `length` and `slice()` directly. A user interface that truncates visible symbols needs grapheme cluster segmentation. Name the counting unit first, and method selection becomes deterministic.

## How it works

When you call `text.toUpperCase()`, JavaScript reads the value of `text` and runs the corresponding method. Transforming methods return new strings; query methods may return a Boolean, index, array, or iterator. The old variable doesn't receive a result automatically, so you must assign or pass the result onward.

You can classify common methods by their result and matching model:

| Task | Preferred method | Key contract |
| --- | --- | --- |
| Test for fixed text | `includes()` | Case-sensitive; returns a Boolean |
| Find a fixed-text position | `indexOf()`, `lastIndexOf()` | Returns `-1` when absent |
| Test a prefix or suffix | `startsWith()`, `endsWith()` | Accepts a position or length argument |
| Extract a range | `slice()` | Excludes the end; accepts negative indices |
| Read one code unit by position | `at()` | Accepts negative indices; returns `undefined` out of range |
| Divide on a separator | `split()` | Capturing groups may enter the result array |
| Replace matches | `replace()`, `replaceAll()` | With a string search value, replaces the first or all matches |
| Remove boundary whitespace | `trim()`, `trimStart()`, `trimEnd()` | Leaves internal whitespace unchanged |
| Pad to a target length | `padStart()`, `padEnd()` | Measures the target in code units |
| Repeat a string | `repeat()` | May throw for an invalid count or oversized result |
| Normalize Unicode | `normalize()` | Defaults to NFC; doesn't perform case conversion |
| Compare for a locale | `localeCompare()`, `Intl.Collator` | Depend only on negative, positive, or zero |

### Immutable results and method chains

A method chain uses one return value as the next receiver. For example, `input.trim().toLowerCase()` first creates a string without boundary whitespace, then converts the case of that result. Chaining doesn't change the semantics of either step or automatically validate an intermediate result.

Not every string method returns a string. `includes()` returns a Boolean, `indexOf()` returns a number, `match()` may return an array or `null`, and `matchAll()` returns an iterator. Generated code that keeps calling string methods without checking return types often fails far from the real mistake.

A primitive string can call prototype methods through automatic boxing, but it doesn't permanently become a `String` object. Application code should normally use primitive strings. `new String("text")` creates an object whose truthiness and strict-equality behavior are easier to misuse.

### Indices and half-open ranges

`slice(start, end)` returns code units from `start` up to, but not including, `end`. Omitting `end` extracts through the end, while negative arguments are translated from the string length. A half-open range means `slice(0, n)` usually has length `n` and adjacent ranges can be written as `slice(0, cut)` and `slice(cut)`.

`substring()` also excludes its end, but treats negative numbers as `0` and swaps the arguments when the start exceeds the end. Unless you're maintaining code that relies on that swapping behavior, new code usually benefits from the more direct semantics of `slice()`. The historical `substr()` method uses a “start plus length” model and shouldn't be generated in new code.

Position arguments are still code-unit indices. An arbitrary `slice()` boundary in text containing surrogate pairs, combining marks, or zero-width joiners can create a lone surrogate or split one visible symbol. Protocol requirements and interface requirements need separate handling.

### Fixed text, regular expressions, and replacement values

Prefer `includes()`, `indexOf()`, `startsWith()`, or `endsWith()` for fixed-text searches. Choose `search()`, `match()`, `matchAll()`, and regular-expression replacement when you need character classes, repetition, or capture groups. This keeps matching semantics and failure modes visible.

With a string search value, `replace()` handles the first occurrence and `replaceAll()` handles every non-overlapping occurrence. With a regular expression, the `g` flag determines global matching. Passing a non-global regular expression to `replaceAll()` throws `TypeError`.

Sequences such as `$&`, `$1`, `$`` and `$'` have special meanings inside a replacement string. If replacement content comes from data and must be inserted literally, pass a replacer function that returns it. The function's return value isn't interpreted again for those substitution markers.

## Examples

These four examples progress from immutable cleanup to fixed-delimiter parsing, literal replacement, and grapheme-safe truncation. Every output shown comes from running the corresponding file with Node 24.14.0.

### 1. Clean a label while preserving its source

`trim()` removes only boundary whitespace, then a regular-expression replacement collapses internal whitespace to one space. The function returns a new value while the input string remains unchanged.

<!-- quick -->

```javascript
// file: clean-label.js
function cleanLabel(value) {
  return value.trim().replace(/\s+/gu, " ");
}

const original = "  Priority\t order  ";
const cleaned = cleanLabel(original);

console.log(JSON.stringify(original));
console.log(JSON.stringify(cleaned));
console.log(original === "  Priority\t order  ");
console.log(cleaned.toUpperCase());
```

```text
"  Priority\t order  "
"Priority order"
true
PRIORITY ORDER
```


<!-- /quick -->

`JSON.stringify()` makes the tab and boundary spaces visible in the output. The third line proves that `cleanLabel()` didn't rewrite `original`; the final line creates another uppercase string from the cleaned result.

This cleanup rule suits a field whose contract says all internal whitespace is equivalent to one space. Code, poetry, preformatted text, and some natural-language content don't satisfy that contract, so don't apply it unconditionally.

### 2. Extract a field at its first delimiter

A header field separates its name and value at the first colon. Locating it with `indexOf()` and then applying two half-open `slice()` operations preserves later colons inside the value.

```javascript
// file: parse-header.js
function parseHeader(line) {
  const separator = line.indexOf(":");
  if (separator === -1) {
    throw new SyntaxError("missing colon");
  }

  const name = line.slice(0, separator).trim().toLowerCase();
  const value = line.slice(separator + 1).trim();
  return { name, value };
}

for (const line of [
  "Content-Type: application/json",
  "Location: https://example.test:8443/orders/7",
  "invalid header"
]) {
  try {
    console.log(JSON.stringify(parseHeader(line)));
  } catch (error) {
    console.log(`${error.name}: ${error.message}`);
  }
}
```

```text
{"name":"content-type","value":"application/json"}
{"name":"location","value":"https://example.test:8443/orders/7"}
SyntaxError: missing colon
```

Calling `line.split(':')` and destructuring its first two entries would lose the URL's port section. This code demonstrates only a controlled format. Real HTTP parsing still belongs to the runtime or a protocol library because the complete field grammar has more rules.

The `-1` returned by `indexOf()` is a sentinel that must be handled. Passing it directly to `slice()` activates negative-index behavior and may turn malformed input into plausible-looking data.

### 3. Insert replacement data literally

The template markers are fixed strings, so `replaceAll()` is sufficient. A function returns each replacement value, preserving content such as `$&` literally.

```javascript
// file: fill-template.js
function fillTemplate(template, values) {
  let result = template;

  for (const [name, value] of Object.entries(values)) {
    const marker = `{{${name}}}`;
    result = result.replaceAll(marker, () => String(value));
  }

  const unresolved = [...result.matchAll(/\{\{(?<name>[a-z]+)\}\}/gu)]
    .map((match) => match.groups.name);
  if (unresolved.length > 0) {
    throw new Error(`unresolved: ${unresolved.join(", ")}`);
  }
  return result;
}

console.log(fillTemplate("Total: {{amount}}", { amount: "$&5" }));

try {
  console.log(fillTemplate("Hello {{name}} from {{team}}", { name: "Mira" }));
} catch (error) {
  console.log(error.message);
}
```

```text
Total: $&5
unresolved: team
```

If this used `replaceAll(marker, String(value))`, `$&` would expand to the matched marker and the first line would incorrectly retain `{{amount}}`. A replacer function disables that layer of replacement-string syntax, but it doesn't automatically give a homemade template system HTML escaping, access control, or safe expression evaluation.

The final `matchAll()` reports unresolved markers after all replacements. It uses a global regular expression and reads field names from a named capture group. If the template grammar grows, replace this with a defined template parser.

### 4. Truncate interface text by grapheme cluster

`Intl.Segmenter` applies Unicode text-segmentation rules to identify symbols users generally perceive. This example compares code-unit, code-point, and grapheme-cluster counts.

```javascript
// file: graphemes.js
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });

function graphemes(text) {
  return Array.from(segmenter.segment(text), ({ segment }) => segment);
}

function truncateGraphemes(text, limit) {
  const parts = graphemes(text);
  return parts.length <= limit ? text : `${parts.slice(0, limit).join("")}…`;
}

const label = "👨‍👩‍👧‍👦 cafe\u0301";
console.log(label.length);
console.log([...label].length);
console.log(graphemes(label).length);
console.log(JSON.stringify(graphemes(label)));
console.log(truncateGraphemes(label, 3));
```

```text
17
13
6
["👨‍👩‍👧‍👦"," ","c","a","f","é"]
👨‍👩‍👧‍👦 c…
```

The family emoji consists of several code points joined by zero-width joiners, while the final `é` combines a letter and a combining mark. Spread syntax iterates by code point, so it still splits both kinds of grapheme cluster. The segmenter treats each as one interface symbol.

The `"en"` argument is an explicit locale. Grapheme boundaries are mostly language-independent, but an explicit locale makes the behavior and call intent clear. Word and sentence segmentation depend on language more strongly.

## Pitfalls

> **Pitfall:** **Ignoring string immutability.** `name.trim()` and `name.toLowerCase()` don't rewrite `name`; generated code often invokes a method and then continues using the old value.
>
> **Fix:** Save the result, as in `const normalized = name.trim()`. When reviewing a chain, trace every receiver, return type, and final use.

> **Pitfall:** **Treating `length` as a visible-character count.** Emoji, some historic scripts, and combining sequences make code-unit, code-point, and grapheme-cluster counts differ. An arbitrary `slice()` may also split a surrogate pair.
>
> **Fix:** State the counting unit in protocol limits. Use the string iterator for code points and `Intl.Segmenter` for interface symbols. Test surrogate pairs, combining marks, and zero-width-joiner sequences.

> **Pitfall:** **Confusing `replace()` with replace-all behavior.** A string search value with `replace()` affects only the first match, while `replaceAll()` requires the `g` flag when it receives a regular expression.
>
> **Fix:** Select the API along two axes: first or all matches, and fixed text or a pattern. Test zero, one, and several matches, and return replacement data from a function.

> **Pitfall:** **Parsing a full grammar with simple splitting or a regular expression.** `split(',')` can't handle quoted CSV fields, a tag-removal regex can't implement HTML parsing rules, and hand-written URL splitting misses encoding and relative references.
>
> **Fix:** Reserve string methods for small formats genuinely defined by fixed delimiters. Use the relevant parser for CSV, HTML, URLs, and other formal grammars, then validate business constraints after parsing.

> **Pitfall:** **Using case conversion for every case-insensitive comparison.** `toLowerCase()` doesn't express linguistic collation rules and doesn't automatically solve canonical equivalence, identifier security, or every multi-character case mapping.
>
> **Fix:** Use an explicitly configured `Intl.Collator` for user-visible search and sorting. Follow the protocol's own ASCII or normalization rules for protocol identifiers, and don't reuse display-locale rules for authorization keys.

> **Pitfall:** **Depending on `localeCompare()` to return exactly `-1` or `1`.** Its contract guarantees a negative number, positive number, or zero; control flow must not depend on the magnitude.
>
> **Fix:** Test comparison results with `< 0`, `> 0`, and `=== 0`. Reuse an `Intl.Collator`'s `compare` for many comparisons under one configuration, and validate the ordering with data from the target locale.

<!-- deep -->

## More than one text boundary

### Code units, code points, and grapheme clusters

An ECMAScript string's `length` is its number of UTF-16 code units. Bracket access, `at()`, `charAt()`, `charCodeAt()`, and slice positions also use code-unit indices. A supplementary-plane code point is represented by a surrogate pair, so `"😀".length` is `2`.

The string iterator, spread syntax, and `Array.from(string)` recognize well-formed surrogate pairs and yield strings by code point. They avoid splitting one supplementary-plane code point in half, but a combining mark, emoji modifier, regional-indicator flag, or zero-width-joiner sequence may still span several results.

A grapheme cluster is closer to what users perceive as “one character.” `Intl.Segmenter` with `granularity: "grapheme"` reports boundaries using Unicode segmentation rules, but it neither changes the source string nor claims every cluster has equal display width. Terminal columns and font layout are separate problems.

| Operation | Counting unit | Suitable requirement |
| --- | --- | --- |
| `text.length` | UTF-16 code units | A JavaScript API's native length contract |
| `text.slice(a, b)` | UTF-16 code units | A protocol range defined in code-unit offsets |
| `[...text]` | Unicode code points | Iteration that doesn't split well-formed surrogate pairs |
| `Intl.Segmenter` in grapheme mode | Grapheme clusters | Selecting or truncating user-visible symbols |
| Canvas or layout APIs | Rendered measurements | Pixel width, wrapping, and layout |

`codePointAt(index)` reads a code point from a code-unit position. If `index` points to the leading half of a surrogate pair, it returns the full code point; if it points to the trailing half, it returns only that trailing surrogate's numeric value. It solves representation-aware reading, not code-point indexing.

`String.fromCodePoint()` constructs a string from code-point values, while `String.fromCharCode()` constructs it from 16-bit code units. Use the former when handling complete Unicode code points. The latter directly matches input only when a protocol supplies raw UTF-16 units.

### Ill-formed Unicode

A JavaScript string can contain a lone surrogate code unit. It remains a valid ECMAScript string but isn't a well-formed sequence of Unicode scalar values. Splitting a surrogate pair, decoding external binary data incorrectly, or explicitly constructing code units can produce such a value.

In Node 24, `isWellFormed()` detects lone surrogates and `toWellFormed()` replaces each one with U+FFFD. That conversion loses the original code unit, so perform it deliberately at a contract boundary rather than hiding it in generic cleanup.

Encoding APIs differ in how they handle ill-formed strings. Define the accepted Unicode form before transmitting text between systems and test lone surrogates. Don't assume every receiver preserves or rejects the same input.

## Search and replacement protocols

### The `indexOf()` sentinel

`indexOf(search, fromIndex)` returns the code-unit index of the first match or `-1` when absent. An empty search string always matches at the clamped start position, so `text.includes("")` is `true`. That call can't validate that an input is nonempty.

Use `includes()` when you only need an existence test. Preserve the `indexOf()` result when you need a slice boundary or continued search. A loop finding every occurrence must also decide whether matches may overlap and how an empty search advances, or it may never terminate.

`startsWith()`, `endsWith()`, and `includes()` target fixed strings and don't accept regular expressions. This restriction exposes a call that confuses patterns and literals. Choose a regular-expression API explicitly when you need a pattern.

### Symbol-method dispatch

`match()`, `matchAll()`, `search()`, `replace()`, `replaceAll()`, and `split()` don't merely convert every argument to a plain string. If an object implements the corresponding `Symbol.match`, `Symbol.matchAll`, `Symbol.search`, `Symbol.replace`, or `Symbol.split`, the operation dispatches to that protocol method. `RegExp` participates in string operations through these symbols.

That means a “pattern” supplied as an untrusted object may execute user code. Most application boundaries should accept a definite string or a controlled `RegExp`; don't pass arbitrary objects to these methods under the assumption that the operation is a pure query.

The shape returned by `match()` depends on the regular expression's `g` flag. A non-global match preserves captures and an index, while a global match primarily returns complete matched strings. Use `matchAll()` with a global expression when you need to iterate through every match and its capture groups.

### Replacer-function arguments

A replacer function receives the complete match, each capture, the match offset, and the original string. A regular expression with named captures also adds a `groups` object. An optional capture that didn't match supplies `undefined`. A generic wrapper can't assume the penultimate argument is always the offset because `groups` changes the tail shape.

Replacement determines match positions from the original input; it doesn't repeatedly rescan newly inserted text. Replacing one item with a value that contains the search text therefore doesn't loop forever by itself. A sequence of `replaceAll()` calls still has order effects because a later step can process content produced by an earlier one.

A string search value is treated literally and needs no escaping for regular-expression metacharacters. When a dynamic requirement really means “replace this fixed text,” passing a string is usually safer than constructing a `RegExp`, and it avoids backslash and flag mistakes.

## Normalization, case, and ordering

### Unicode normalization

Visually identical text may use different code-point sequences. For example, precomposed `"é"` and `"e\u0301"` aren't strictly equal. `normalize("NFC")` converts many canonically equivalent sequences to one composed form. NFD, NFKC, and NFKD have different decomposition and compatibility semantics.

Normalization isn't cleanup, translation, or a security filter. Compatibility normalization may collapse typographic distinctions, while case conversion is a separate operation. An application must define the order and target form and apply them consistently to writes, queries, and uniqueness checks.

Don't normalize received text implicitly before verifying a signature or hash because any code-unit change alters its byte representation. If a protocol requires a normalization form, apply it at the point defined by that protocol and make both parties use the same encoding and version rules.

### Locale-sensitive case conversion

`toLowerCase()` and `toUpperCase()` use default Unicode case mappings and don't accept a locale. `toLocaleLowerCase()` and `toLocaleUpperCase()` accept locale arguments and can handle locale-specific mappings such as Turkish I.

A case mapping can change length and isn't guaranteed to round-trip to the source. The uppercase form of German lowercase `ß` expands to multiple code points. You therefore can't align a case-converted string with its source by reusing source indices.

Protocol keywords, programming-language identifiers, and security tokens usually define their own case rules. Applying the user's locale to these values makes results vary with the environment or account language. Follow the protocol first, and reserve locale-sensitive methods for display text.

### Comparison and sorting

`localeCompare()` suits occasional locale-aware comparisons. For a large sort, create one `Intl.Collator(locale, options)` and reuse its `compare`. Options such as `sensitivity`, `numeric`, `caseFirst`, and `usage` change which differences participate in comparison and must come from product requirements.

Collation equality isn't strict string equality. A collator configured to ignore accents or case may return `0` for strings with different code units. Search hits, deduplication, unique keys, and authorization checks shouldn't share one loose comparator without a contract.

Ordering also depends on the runtime's internationalization data. If several services or versions must produce a stable persistent order, save an explicit sort key or define a server-side ordering contract instead of assuming every environment has the same default locale.

## Testing method boundaries

Test string methods by contract partitions rather than a few ordinary English words. At minimum, cover empty boundaries, absent states, starts and ends, repeated matches, and return types. For external input, also decide whether non-string values are rejected or explicitly converted.

Index-related tests should separately include a BMP character, supplementary-plane code point, combining sequence, and zero-width-joiner sequence. Those four inputs reveal whether code really operates on code units, code points, or grapheme clusters. They also catch tests titled “characters” whose assertions cover ASCII only.

Replacement tests should cover no match, one match, repeated matches, an unmatched capture, and replacement data containing `$&` or `$1`. A dynamic regular expression also needs metacharacter and backslash cases to verify whether the requirement is a literal or a pattern.

Locale-aware tests must fix the locale and options. Snapshots that depend on the machine's default locale can drift among a workstation, CI, and user devices. Assert only the sign or zero of `localeCompare()`, never a particular `-1` or `1`.

Every step of a method chain should preserve meaningful failure information. Casually converting `match()`'s `null` to an empty array, or a missing delimiter to an empty string, may hide malformed input. If absence is valid, express that branch in the interface contract.

Finally, test whether the original input should remain unchanged. A string is immutable, but arrays and objects containing strings are still mutable. `records.sort()` mutates the array even though its comparator reads immutable strings, so don't project string semantics onto an outer container.

<!-- /deep -->

[Checkpoint: javascript/string-methods](https://codewiki.com/javascript/string-methods/#checkpoint)

## Further reading

- [MDN: `String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)
- [MDN: `String.prototype.slice()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice)
- [MDN: `String.prototype.replace()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace)
- [MDN: `Intl.Segmenter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter)
- [ECMAScript specification: String objects](https://tc39.es/ecma262/multipage/text-processing.html#sec-string-objects)
- [Unicode Standard Annex 29: Text Segmentation](https://www.unicode.org/reports/tr29/)
