Skip to content
JavaScript js regex 4 min read

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.

SyntaxNameMeaning
(?=...)Positive lookaheadFollowed by ...
(?!...)Negative lookaheadNot followed by ...
(?<=...)Positive lookbehindPreceded by ...
(?<!...)Negative lookbehindNot 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.
Last updated June 1, 2026
Was this helpful?