(...) capturing groups: numbering and what comes back
Parentheses group part of a pattern and record the text it matched. Groups are numbered by the position of their opening parenthesis, left to right, with group zero being the whole match.
At a glance
- Syntax
- (...)
- Numbering
- by the position of the opening parenthesis, counting from the left
- Group 0
- the whole match, always present
- Unmatched group
- undefined, never an empty string
- Quantified group
- keeps only the text of the final iteration
- When not to capture
- use (?:...) when you only need grouping, so later numbers stay stable
In detail
Numbering follows the opening parenthesis, which makes nested groups easy to read once you know the rule: the outer group is always numbered before the inner one. Matching /((a)(b))/ against "ab" gives four entries, the whole match, then the outer group holding ab, then a and b. Counting closing parentheses instead is the usual source of an off by one.
A group that did not take part is undefined rather than an empty string. In /(a)|(b)/ exactly one branch matches, so the other group's slot holds undefined, and code that calls a string method on it throws. That is a real distinction: an empty string means the group matched nothing, while undefined means the group was never reached.
Quantifying a capturing group repeats the match but not the capture. /(\d){3}/ matches three digits and leaves only the last one in group one, because each iteration overwrites the previous value. When the whole run is what you want, the quantifier belongs inside the parentheses, as in /(\d{3})/.
Capturing has a cost in both directions. It makes the engine record positions it might not need, and it fixes the numbering of everything after it, so inserting a group in the middle of a pattern renumbers every later reference and every $1 in a replacement string. Anything grouped purely to apply a quantifier should be non capturing.
Examples
Array.from(/(\d{4})-(\d{2})-(\d{2})/.exec("2026-03-08"));
// ["2026-03-08", "2026", "03", "08"]
Array.from(/((a)(b))/.exec("ab"));
// ["ab", "ab", "a", "b"]const m = /(a)|(b)/.exec("b");
m[0]; // "b"
m[1]; // undefined, this branch was never taken
m[2]; // "b"Things that catch people out
- A group in an alternative branch that was not taken is undefined. Reading .length from it throws, and the stack trace points at your code rather than at the pattern.
- Quantifying a group keeps only the last iteration, so /(\d){3}/ reports one digit where the author expected three.
- Adding a group in the middle of a pattern renumbers everything after it, including any $1 style tokens in a replacement string somewhere else in the file.
Try it: Regex Tester
Type a pattern and a test string, watch every match highlight as you go, read a plain English breakdown of each token, and check the numbered and named groups it captured.
Regex Tester
LOCAL · Browser-OnlyTest and debug regular expressions with live matching and highlights
Related Regex syntax
All regex syntax →(?:...): grouping without creating a capture
A non capturing group applies quantifiers and alternation without taking a number, which is what keeps $1 pointing at the same thing after an edit.
(?<name>...): named capture groups
Named groups survive being reordered, read as documentation and give you $<name> in replacements. They are still numbered underneath.
\1: backreferences that match what a group captured
A backreference matches the exact text a group captured, not the pattern that captured it. Inside a character class the same escape means something else.
\k<name>: backreferences to a named group
\k<name> repeats what a named group captured. Get the name wrong and, without the u flag, the escape quietly becomes a literal letter k.
(?=...): positive lookahead for zero width tests
A lookahead asserts what comes next without consuming it, which is how several independent rules can all be checked from the same position.
(?!...): negative lookahead for exclusion rules
A negative lookahead succeeds when the pattern inside it fails. The classic bug is a greedy quantifier that backtracks until the assertion passes.
Tools for this
- Regex Tester Free online regex tester - test and debug regular expressions with live matching and highlights
- Regex Playground Regex playground with live match highlighting, named groups, find and replace, and shareable links