String.prototype.matchAll()
Returns an iterator of every regex match, each with its capture groups and index.
matchAll(regexp)- Added in JavaScript ES2020
Parameters
| Parameter | Type | Description |
|---|---|---|
$regexp | RegExp | { [Symbol.matchAll](): any } | A regular expression object, or any object with a Symbol.matchAll method. A value that is neither is implicitly converted with new RegExp(regexp, 'g'). A regex argument must have the global (g) flag set. |
Return value
RegExpStringIteratorAn iterable iterator object (which is not restartable) of matches, or an empty iterator if no matches are found. Each yielded value is an array with the same shape as the return value of RegExp.prototype.exec().
Errors
Throws a TypeError if regexp is a regex whose flags do not contain "g".
Changelog
- PHP ES2020Introduced, replacing the while (m = re.exec(str)) loop and its lastIndex bookkeeping.
matchAll() is the answer to the one thing match() cannot do: give you every match and its capture groups. Each yielded value is a full exec()-style result — [0] the whole match, [1..n] the groups, plus index, input and groups.
The g flag is mandatory
A regex without g throws a TypeError immediately. Without the flag there is no sensible "all matches" semantics, so the specification refuses rather than guessing.
It returns an iterator, not an array
You cannot index into it or read .length. Either for...of it, or spread it into an array with [...str.matchAll(re)]. The iterator is not restartable — once consumed it yields nothing, so if you need two passes, materialise it into an array first.
Why it beats a manual exec loop
The old idiom, while ((m = re.exec(s)) !== null), mutates the regex's lastIndex. Sharing that regex elsewhere, or forgetting to handle a zero-length match, causes infinite loops and phantom bugs. matchAll() internally clones the regex, so your original object's lastIndex is left alone.
Example
When to use it
- Extract every key/value pair from a query string with groups
- Find all markdown links and their positions in a document
- Collect every timestamp in a log with its offset
- Tokenise an expression while keeping each token's index
Frequently asked questions
Why does matchAll throw a TypeError?
Because the regular expression is missing the global g flag, which matchAll() requires by specification. Change /foo/ to /foo/g; there is no non-global meaning for an all-matches iterator.
How do I turn matchAll results into an array?
Spread the iterator with [...str.matchAll(re)] or use Array.from(str.matchAll(re)). The iterator itself has no length property and cannot be indexed, and it can only be walked once.
When should I use matchAll instead of match?
Use matchAll() whenever you need capture groups, the index, or the groups object for every match, since match() with the g flag discards all of those and returns only the matched strings. Use match() without g when one match is enough.
More examples
Parsing key/value pairs with named groups
The output is { page: "2", sort: "name", dir: "desc" }. Because every match carries its own groups object, one pass turns the query string into a plain object — something match() with /g/ cannot do since it strips the groups.
const qs = "page=2&sort=name&dir=desc";
const re = /(?<key>\w+)=(?<value>[^&]+)/g;
const params = Object.fromEntries(
[...qs.matchAll(re)].map((m) => [m.groups.key, m.groups.value])
);
console.log(params);
console.log(params.sort);The iterator is single-use
The second spread yields 0 because the iterator was exhausted by the first. Storing the spread result in a variable gives 2 and 2, which is why any code that both counts and loops must materialise the array up front.
const it = "x1 x2".matchAll(/x(\d)/g);
console.log([...it].length); // 2
console.log([...it].length); // 0 — already consumed
// Materialise once, reuse freely
const all = [..."x1 x2".matchAll(/x(\d)/g)];
console.log(all.length, all.length);Replacing the exec loop
Each match destructures straight into full, user and host. The final 0 is the important part: matchAll clones the regex internally, so the original object's lastIndex is never touched. A hand-written exec loop mutates lastIndex as it goes — it only returns to 0 if you run the loop until exec() returns null, so breaking out early leaves it parked at the end of the last match and reusing that same regex object elsewhere silently starts matching from the middle of the next string.
const re = /(\w+)@(\w+)\.com/g;
const text = "[email protected] and [email protected]";
// matchAll leaves the original regex's lastIndex alone
for (const [full, user, host] of text.matchAll(re)) {
console.log(full, user, host);
}
console.log(re.lastIndex); // 0
Discussion