skip to content

Matching and Replacing APIs

The API surface: RegExp.test/exec versus String match, matchAll, search, split, replace and replaceAll — including function replacers. Interviewers ask because a /g regex is stateful, and reusing one across calls produces the classic alternating-results bug.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In JavaScript, what does String.prototype.replace do when you pass a plain string as the pattern, and how do replaceAll and a global regex differ from it?

level: juniorimportance: must knowfreq 72%

answer

  1. one occurrence unless told otherwise
  2. the g flag is what repeats it
  3. replaceAll takes a literal string
  4. non-global regex into replaceAll throws
  5. dollar signs in the replacement expand

basics

~20 s

replace() with a string pattern, or with a regex that lacks the g flag, substitutes only the first occurrence. Replacing every occurrence needs replaceAll() or a /g regex; replaceAll throws a TypeError if handed a non-global regex.

solid answer

~40 s

`String.prototype.replace(pattern, replacement)` returns a new string — strings are immutable — and it replaces exactly one occurrence when the pattern is a plain string or a regex without the `g` flag. That is the classic surprise: `'a-b-c'.replace('-', '+')` gives `'a+b-c'`. To hit every occurrence you either add the global flag (`'a-b-c'.replace(/-/g, '+')`) or use `replaceAll`, added in ES2021, which takes a string pattern and replaces all matches with no regex and no escaping of metacharacters. `replaceAll` deliberately throws a `TypeError` if you pass it a regex without `g`, because "replace all, but only the first one" is a contradiction. One more thing to know: the replacement string is not literal — `$&`, `$1`, `$$` and friends are expanded, so a `$` coming from user data can splice unexpected text into the result.

code

javascript · 13 lines
javascript
const s = 'a.b.c';

console.log(s.replace('.', '-'));      // 'a-b.c'  first only
console.log(s.replaceAll('.', '-'));   // 'a-b-c'  literal dot
console.log(s.replace(/\./g, '-'));    // 'a-b-c'  escaped dot, global
console.log(s.replace(/./g, '-'));     // '-----'  unescaped dot = any char
console.log(s);                        // 'a.b.c'  never mutated

try {
  s.replaceAll(/\./, '-');
} catch (e) {
  console.log(e.constructor.name);     // 'TypeError'
}

go deeper

for a junior

Know that replace() with a string pattern changes only the first occurrence and returns a new string. Say plainly that replaceAll() or a /g regex is what replaces all of them.

for a middle

Explain why replaceAll rejects a non-global regex, and why replacing a variable substring with a hand-built regex needs metacharacter escaping while replaceAll with a string does not.

for a senior

Show the $-expansion trap: replacement strings that come from data can splice matched text into the output. Demonstrate the two fixes — doubling the dollar signs or using a replacer function.

for a principal

Own the codebase-wide rule: literal replacements go through replaceAll with a string, patterns go through reviewed regexes, and no untrusted value is ever concatenated into a pattern or a replacement string.

## What replace actually is `String.prototype.replace(pattern, replacement)` takes a pattern (a string or a `RegExp`) and a replacement (a string or a function) and **returns a new string**. It never modifies the receiver, because JavaScript strings are immutable values — if you ignore the return value, nothing appears to happen: ```js const s = 'a-b-c'; s.replace('-', '+'); console.log(s); // 'a-b-c' — unchanged console.log(s.replace('-', '+')); // 'a+b-c' ``` ## A string pattern replaces exactly one occurrence When `pattern` is a plain string, `replace` finds the **first** occurrence and substitutes it. There is no "all" option and no third argument — `s.replace('-', '+', 'g')` silently ignores the extra argument and still replaces one. This is the single most common beginner bug with the method, and it is easy to miss because the code looks right and works on inputs that happen to contain one match. ## The global flag makes it repeat If the pattern is a `RegExp` carrying the `g` flag, `replace` walks the whole string and substitutes every non-overlapping match, left to right: ```js 'a-b-c'.replace(/-/g, '+'); // 'a+b+c' 'a-b-c'.replace(/-/, '+'); // 'a+b-c' ``` Under the hood, `replace` delegates to the pattern's `Symbol.replace` method when the pattern has one, which is how `RegExp` customises the operation. That implementation sets the regex's `lastIndex` to 0 before it starts when the regex is global, so a shared global regex does not make `replace` skip matches the way it can make `test` and `exec` behave oddly. ## replaceAll (ES2021) `replaceAll` gives you "every occurrence" without a regex: ```js '1.2.3'.replaceAll('.', '-'); // '1-2-3' '1.2.3'.replace(/./g, '-'); // '-----' — `.` is "any character" ``` That second line is the reason `replaceAll` was added. Before it existed, replacing every occurrence of a *variable* substring meant constructing a regex from that string and escaping every character that means something special in a pattern; forget the escaping and the substitution quietly does the wrong thing, as `.` does above. `replaceAll` with a string pattern compares literally, so no escaping question arises. `replaceAll` also accepts a `RegExp`, but only a global one. Passing a non-global regex throws: ```js 'aaa'.replaceAll(/a/, 'b'); // TypeError: replaceAll must be called with a global RegExp ``` The guard exists because the two halves of the call would contradict each other, and a silent choice between them would be a bug factory. ## The replacement string is not taken literally Both methods expand `$` sequences in a replacement **string**: `$$` inserts a literal `$`, `$&` the matched substring, `` $` `` the text before the match, `$'` the text after it, `$1`…`$n` the numbered captures, and `$<name>` a named capture. So a replacement value that came from user input or from a password generator can inject text you never intended: ```js 'price'.replace('price', "$&$&"); // 'priceprice' ``` Two safe routes: double every `$` in the untrusted value (`value.replaceAll('$', '$$$$')`), or pass a **function** as the replacement — its return value is used verbatim, with no `$` expansion at all. ## Choosing between them - Replacing one known occurrence, or you genuinely want only the first: `replace` with a string. - Replacing every occurrence of a literal substring, especially one held in a variable: `replaceAll` with a string. - The pattern is a real pattern (classes, quantifiers, alternation): a regex — add `g` when you want all of them. - The replacement depends on what matched: a replacer function, with either method. ## Availability `replace` has been in the language since ES3. `replaceAll` is ES2021 — universally available in current browsers and Node, but it is the one to check if you target a very old runtime; the fallback is `split(sep).join(rep)` for a literal separator or an escaped global regex.

  • Why does replaceAll throw on a non-global regex instead of just replacing everything anyway?
    Because the call would be self-contradictory: the method name says every match, the missing `g` flag says one. Rather than silently picking a winner — which would hide a real mistake in the caller's pattern — the spec makes it a `TypeError` you see immediately. A global regex expresses the same intent unambiguously, so nothing is lost.
  • Before replaceAll existed, what was the idiomatic way to replace every occurrence of a substring held in a variable?
    Either `str.split(sep).join(rep)`, which compares literally and needs no escaping, or building a global regex from the variable with every metacharacter escaped first. The second is what people usually reached for and usually got wrong: an unescaped `.`, `+`, `(` or `?` from the variable silently changes the pattern's meaning. `replaceAll` removed the whole problem.
  • How do you insert a literal dollar sign through the replacement string?
    Write `$$`. In a JavaScript source literal that is `'$$'`, so `'x'.replace('x', '$$')` yields `'$'`. If the replacement text is data rather than something you wrote, double every `$` in it first, or pass a replacer function — a function's return value is used verbatim with no `$` expansion.

saying these in an interview costs you the question

  • Thinks replace always replaces every occurrence
  • Passes 'g' as a third argument to replace
  • Uses replace(/./g, x) expecting a literal dot
  • Assumes replace mutates the original string
  • Thinks replaceAll silently works with a non-global regex

context

open as a page

A module declares `const RE = /^\d+$/g;` and a validate(value) function that returns RE.test(value). Callers report that the same valid value passes on one call and fails on the next. Why, and how do you fix it?

level: middleimportance: must knowfreq 58%

basics

~20 s

A regex with the g flag carries mutable state: test and exec resume from its lastIndex property and advance it after each match, so a shared regex object alternates between matching and failing. Drop the g flag for a pure test, reset RE.lastIndex = 0, or build a fresh regex per call.

open as a page

In JavaScript, how does String.prototype.match behave with and without the g flag, and what does String.prototype.matchAll give you that match does not?

level: middleimportance: must knowfreq 62%

basics

~20 s

Without the g flag, match returns a rich result for the first match — the matched text, its captures, index and input. With g it returns a flat array of matched strings and drops all of that detail. matchAll returns an iterator of the rich results for every match, and requires a global regex.

open as a page

In JavaScript's String.prototype.replace, what arguments does a replacer function receive, and when would you use one instead of a replacement string containing $& or $1?

level: middleimportance: should knowfreq 46%

basics

~20 s

A replacer function is called once per match with the whole matched text, then each capture group, then the match offset and the full input string (plus a groups object when the pattern has named captures). Its return value is inserted verbatim, with no dollar-sign expansion.

open as a page

Two variants of the loop `while ((m = re.exec(s)) !== null) { … }` hang forever: one where `re` is `/\d/` and one where `re` is `/\d*/g`. Explain each hang, and say what you would write instead.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Without the g flag, exec always restarts at index 0 and returns the same first match forever. With g but a pattern that can match the empty string, a zero-length match leaves lastIndex unchanged, so the loop re-matches at the same position. Prefer str.matchAll(re), which handles both.

open as a page