Groups, Capturing & Lookaround
Matching a pattern is only half the story — most of the time you also want to pull specific pieces out of the text, reuse part of a match later in the same pattern, or match something only when it is surrounded by the right context. Groups and lookaround are the regex features that make all of this possible. With parentheses you carve a match into named or numbered sub-parts; with lookahead and lookbehind you assert what comes before or after without consuming it. This page walks through capturing groups, non-capturing groups, named groups, backreferences, alternation, and the four lookaround forms.
Capturing groups
A capturing group is any pattern wrapped in parentheses ( ). It does two things: it groups sub-patterns so quantifiers apply to the whole unit, and it remembers the matched text so you can retrieve it afterwards. Captured groups appear as extra entries in the array returned by RegExp.prototype.exec and String.prototype.match (without the global flag).
const date = "2026-06-01";
const re = /(\d{4})-(\d{2})-(\d{2})/;
const result = re.exec(date);
console.log(result[0]); // full match
console.log(result[1]); // first group
console.log(result[2]); // second group
console.log(result[3]); // third group
Output:
2026-06-01
2026
06
01
Index 0 is always the entire match; groups are numbered from 1 in the order their opening parenthesis appears, left to right.
Non-capturing groups
Sometimes you need parentheses purely for grouping — to apply a quantifier or alternation — but you do not want the overhead or numbering shift of a real capture. Prefix the group with ?: to make it non-capturing.
const re = /(?:ab)+(\d)/;
const result = re.exec("ababab7");
console.log(result[0]); // whole match
console.log(result[1]); // only the captured digit
Output:
ababab7
7
The (?:ab) group lets + repeat the pair without claiming a capture slot, so the digit stays at index 1.
Reach for non-capturing groups whenever a group exists only to control precedence. It keeps your capture indices stable and signals intent to the next reader.
Named capturing groups
Numeric indices become hard to read once a pattern has several groups. Named groups, written (?<name>...), let you label captures and access them through the groups object on the result.
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { groups } = re.exec("2026-06-01");
console.log(groups.year);
console.log(groups.month);
console.log(groups.day);
Output:
2026
06
01
Named groups also shine inside String.prototype.replace, where you can reference them with $<name>:
const iso = "2026-06-01".replace(
/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
"$<d>/$<m>/$<y>"
);
console.log(iso);
Output:
01/06/2026
Backreferences
A backreference matches the same text a previous group captured. Use \1, \2, … for numbered groups, or \k<name> for named ones. This is the standard way to find repeated or paired content.
const doubled = /\b(\w+)\s+\1\b/;
console.log(doubled.test("the the cat")); // repeated word
console.log(doubled.test("the cat")); // no repeat
const tag = /<(?<t>\w+)>.*?<\/\k<t>>/;
console.log(tag.test("<p>hi</p>")); // matching open/close
console.log(tag.test("<p>hi</b>")); // mismatched tags
Output:
true
false
true
false
Alternation
The | operator means “match either side.” It has very low precedence, so wrap alternatives in a group to bound them precisely — otherwise the alternation spans the entire pattern.
const wrong = /^cat|dog$/; // ^cat OR dog$
const right = /^(?:cat|dog)$/; // exactly "cat" or "dog"
console.log(wrong.test("catfish")); // true — matches ^cat
console.log(right.test("catfish")); // false
console.log(right.test("dog")); // true
Output:
true
false
true
Lookahead and lookbehind
Lookaround assertions test for context without including it in the match — the matched text is not consumed, so the engine stays in place. There are four forms.
| Syntax | Name | Meaning |
|---|---|---|
(?=...) | Positive lookahead | Followed by ... |
(?!...) | Negative lookahead | Not followed by ... |
(?<=...) | Positive lookbehind | Preceded by ... |
(?<!...) | Negative lookbehind | Not preceded by ... |
A common use is extracting a number while requiring (but not capturing) its surrounding symbols:
// price digits that follow a "$" and precede ".99"
const price = "Total: $49.99";
const amount = price.match(/(?<=\$)\d+(?=\.99)/);
console.log(amount[0]);
// a word NOT followed by "ing"
const re = /\b\w+(?!ing\b)\b/g;
console.log("running walk jumping".match(re));
Output:
49
[ 'walk' ]
Lookahead and lookbehind both have negative variants, letting you express constraints like “a password with at least one digit” succinctly:
const strong = /^(?=.*\d)(?=.*[A-Z]).{8,}$/;
console.log(strong.test("Password1")); // has digit + uppercase, 8+ chars
console.log(strong.test("password")); // fails the assertions
Output:
true
false
Lookbehind (
(?<=...)/(?<!...)) is well supported in modern browsers and Node, but if you target very old runtimes, confirm support or rewrite using a captured prefix group.
Best Practices
- Use
(?:...)non-capturing groups whenever a group only controls precedence — it keeps capture indices meaningful. - Prefer named groups
(?<name>...)over numeric indices in non-trivial patterns; they document the pattern and survive reordering. - Always parenthesize alternation
(?:a|b)so|does not silently swallow the rest of the pattern. - Reach for lookahead/lookbehind to assert context instead of capturing-and-discarding text you do not actually want in the match.
- Combine stacked lookaheads (
(?=.*x)(?=.*y)) for “must contain all of” rules rather than writing brittle character classes. - Remember that lookaround is zero-width: it never advances the match position, which is what makes assertions composable.