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?
answer
- the g flag changes the return shape
- strings only, or match objects
- index and captures survive in one form
- matchAll needs the global flag
- matchAll hands back a lazy iterator
basics
~20 sWithout 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.
solid answer
~40 s`str.match(re)` behaves in two different ways depending on the regex. Without `g`, it returns a single match object — an array whose element 0 is the whole match, whose remaining elements are the capture groups, and which carries `index`, `input` and `groups` properties — or `null` if nothing matched. With `g`, it throws all of that away and returns a flat array of just the matched substrings, or `null`. That is the gap `matchAll` fills, added in ES2020: it returns an **iterator** of full match objects, one per match, so you get every match *and* its index and captures. `matchAll` requires the `g` flag and throws a `TypeError` without it, and unlike a manual `exec` loop it does not leave the original regex's `lastIndex` advanced. Spread it or `for…of` it: `[...str.matchAll(/\d+/g)]`.
code
javascript · 17 linesconst re = /(\w)(\d)/;
const gre = /(\w)(\d)/g;
const s = 'a1 b2';
const one = s.match(re);
console.log(one[0], one[1], one.index); // 'a1' 'a' 0
console.log(s.match(gre)); // ['a1', 'b2'] — captures gone
for (const m of s.matchAll(gre)) {
console.log(m[0], m[1], m[2], m.index);
}
// a1 a 1 0
// b2 b 2 3
console.log('zzz'.match(gre)); // null
console.log([...'zzz'.matchAll(gre)]); // []go deeper
Recall that match returns null when nothing matches, and that adding the g flag turns the result into a plain list of matched strings. Always guard the null.
Explain precisely what the non-global match object carries — element 0, the captures, index, input — and why the global form loses all of it, which is exactly the gap matchAll fills.
Show why matchAll is the safe default in shared code: it clones the regex so a module-level pattern's lastIndex is never left advanced, it handles zero-length matches, and it returns an empty spread instead of null.
Set the house convention: one matching idiom across the codebase rather than a mix of exec loops, global match and matchAll, so nobody has to reason about which call mutated a shared regex.
## Two methods with two different jobs The string-side matching APIs answer different questions, and the trap is that `match` answers two of them depending on a flag. ## match without g: one rich result When the regex is not global, `str.match(re)` is essentially `re.exec(str)`: it returns a **match object** or `null`. ```js const m = 'order 42 of 7'.match(/(\d+)/); m[0]; // '42' the whole match m[1]; // '42' the first capture group m.index; // 6 where the match starts m.input; // 'order 42 of 7' ``` That object is an ordinary `Array` with extra own properties bolted on: `index`, `input`, and `groups` (which is `undefined` unless the pattern declares named captures). Because it is array-like, destructuring works: `const [, id] = str.match(/id=(\d+)/) ?? []`. If nothing matches, the return value is `null`, not an empty array — so `str.match(re).length` throws on a miss. Guard it, or use `?? []`. ## match with g: many results, no detail Add the `g` flag and the return value changes shape entirely: ```js 'order 42 of 7'.match(/(\d+)/g); // ['42', '7'] ``` You now get every match, but only the matched substrings: no `index`, no `input`, and the capture group is gone even though the pattern declares one. This is a genuine information loss, not a formatting difference. It is also the reason people used to write `exec` loops by hand. (One detail worth knowing: the global form resets the regex's `lastIndex` to 0 before it starts and leaves it at 0 afterwards, so `match` is not a source of the shared-regex statefulness bug that `test` and `exec` cause.) ## matchAll: every match, with detail `String.prototype.matchAll`, added in ES2020, returns an **iterator** of the same rich match objects `exec` produces — one per match: ```js for (const m of 'order 42 of 7'.matchAll(/(\d+)/g)) { console.log(m[0], m[1], m.index); } // '42' '42' 6 // '7' '7' 12 ``` Three properties matter in an interview: 1. **It requires the `g` flag.** `'abc'.matchAll(/b/)` throws `TypeError: matchAll must be called with a global RegExp`. The design intent is that "all matches" and a non-global regex contradict each other, and a silent single result would hide the mistake. 2. **It does not disturb the original regex.** The spec makes it operate on a *clone* of the regex you passed, seeded with the original's `lastIndex`. So passing a shared module-level regex to `matchAll` does not leave that regex's `lastIndex` pointing into the middle of some old string. 3. **It is lazy.** Nothing is matched until you pull. Spread it (`[...str.matchAll(re)]`) when you want an array, or `for…of` it when you want to stop early on a huge input. It is also a one-shot iterator: consuming it twice yields nothing the second time, which surprises people who store the return value and iterate it in two places. `matchAll` also advances correctly over zero-length matches, where a hand-rolled `exec` loop would spin forever unless you bump `lastIndex` yourself. ## Picking the right call - Do I just want to know *whether* it matches? `re.test(str)` — cheapest, returns a boolean. - Where does the first match start? `str.search(re)` — returns the index or `-1`, and ignores `g` entirely. - One match with its captures? `str.match(re)` with a non-global regex, or `re.exec(str)`. - Just the list of matched strings? `str.match(re)` with `g`. - Every match *with* captures or positions? `[...str.matchAll(re)]`. ## The null-versus-empty asymmetry `match` returns `null` on no match; `matchAll` returns an iterator that simply yields nothing, so `[...str.matchAll(re)]` is `[]`. That makes `matchAll` friendlier for downstream array code — no null check, no `?? []` — and is a small but real reason to prefer it when you were going to map over the results anyway.
- You call str.match(re) and then read .length on the result. What input breaks that code?Any input with no match: `match` returns `null`, not an empty array, so reading `.length` throws a `TypeError`. Guard with `const m = str.match(re); if (!m) …`, or write `(str.match(re) ?? []).length`. `matchAll` avoids the asymmetry entirely — spreading it on a non-matching string yields `[]`.
- Why does matchAll refuse a non-global regex, when match happily accepts one?`match` has a defined meaning for both cases — one rich result without `g`, a list of strings with it. `matchAll` has only one meaning, and a non-global regex would make it either return exactly one result (contradicting the name) or loop forever on a regex whose `lastIndex` never advances. Throwing a `TypeError` surfaces the caller's mistake immediately.
- If you store the value returned by matchAll and iterate it twice, what happens?The second iteration yields nothing. `matchAll` returns a one-shot iterator, not an array, and iterators are exhausted once consumed. If you need the results more than once, materialise them first with `const all = [...str.matchAll(re)]` and iterate that array.
saying these in an interview costs you the question
- Thinks match always returns index and capture groups
- Expects an empty array instead of null on no match
- Calls matchAll with a non-global regex
- Believes matchAll returns an array you can index
- Iterates the matchAll result twice and expects results both times