SKU // 90

JavaScript RegExp Groups

Parentheses in a regex do two jobs. They group pieces together so you can apply a quantifier or alternation to the whole group, and they capture the text that matched so you can pull it out or reuse it. Capturing is what turns regex from "does it match?" into "give me the parts."

// Capturing Groups

Without the g flag, match returns the full match at index 0, then each group in order:

const m = "Due: 2026-10-02".match(/(\d{4})-(\d{2})-(\d{2})/);

console.log(m[0]);   // "2026-10-02"  the whole match
console.log(m[1]);   // "2026"        group 1
console.log(m[2]);   // "10"          group 2
console.log(m[3]);   // "02"          group 3
2026-10-02 2026 10 02

Groups are numbered by the position of their opening parenthesis, counting from the left.

// Destructuring makes it tidy (skip index 0 with a leading comma)
const [, year, month, day] = "2026-10-02".match(/(\d{4})-(\d{2})-(\d{2})/);

exec() works the same way

The regex method exec() (more in RegExp Objects) returns the same kind of array, plus the position where the match started:

const result = /(\w+) loves (\w+)/.exec("Alice loves Bob-");

console.log(result[0]);
console.log(result[1], result[2]);
console.log(result.index);
Alice loves Bob Alice Bob 0

// Named Groups

Numbers get confusing with many groups. Give each one a name with (?<name>...), then read it from .groups:

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { year, month, day } = "2026-10-02".match(re).groups;

console.log(`${month}/${day}/${year}`);
10/02/2026
Tip
Prefer named groups whenever there are two or more. The code documents itself, and adding a new group later does not shift all the numbers.
W3Schools error
W3Schools says match.groups() "returns an object." groups is a property, not a method: read it without parentheses, like match.groups.firstName. Calling it throws TypeError: ... .groups is not a function.

// Using Groups in replace()

In the replacement string, $1, $2... insert numbered groups and $<name> inserts named ones:

"2026-10-02".replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1");   // "10/02/2026"

"Lovelace, Ada".replace(/(?<last>\w+), (?<first>\w+)/, "$<first> $<last>");   // "Ada Lovelace"
10/02/2026 Ada Lovelace

// Grouping Without Capturing: (?:...)

Sometimes you only need parentheses for grouping. (?:...) groups but does not capture, keeping the numbering clean:

// Repeat a whole group
/(?:ha)+/.test("hahaha")      // true: "ha" one or more times

// Limit an alternation
/^(?:cat|dog)$/.test("dog")   // true: exactly "cat" or "dog"

// Only the second group is captured
"img.png".match(/(?:\w+)\.(\w+)/)[1]   // "png"

// Backreferences: Matching the Same Text Again

Inside the pattern, \1 means "whatever group 1 matched." This finds repeated words:

const text = "This is is a test of the the checker";

text.match(/\b(\w+) \1\b/g);                      // ["is is", "the the"]
text.replace(/\b(\w+) \1\b/g, "$1");              // "This is a test of the checker"
["is is", "the the"] This is a test of the checker
// Named version: \k<name>
/(?<q>["']).*?\k<q>/   // a quote, text, then the SAME kind of quote

// Two Group Traps

A repeated group keeps only its last repetition, and a group that did not take part in the match gives undefined:

console.log("abc".match(/(\w)+/));        // the + repeats the group 3 times

const m = "color".match(/colo(u)?r/);
console.log(m[1]);                           // the optional group matched nothing
console.log("color".replace(/colo(u)?r/, "[$1]"));
["abc", "c"] undefined []
Watch out
  • Repeated group: (\w)+ captures one character at a time and keeps the final one. To capture the whole run, put the quantifier inside: (\w+).
  • Optional group: a group followed by ? is undefined when it did not match (in a replace string, $1 just becomes empty). Check with === undefined before calling string methods on it.

// Every Match, With Its Groups: matchAll

With the g flag, match returns only the full matches and drops the groups. Use matchAll to get groups for every match:

const log = "ava=92, ben=85, cal=78";

for (const m of log.matchAll(/(?<name>\w+)=(?<score>\d+)/g)) {
  console.log(m.groups.name, Number(m.groups.score));
}
ava 92 ben 85 cal 78

// Where Each Group Was Found: the d Flag

As promised in RegExp Flags: with d, the result's indices holds a [start, end] pair for the whole match and for every group. Named groups also appear under indices.groups:

const m = "Due: 2026-10-02".match(/(?<y>\d{4})-(?<mo>\d{2})/d);

console.log(m[0], m.indices[0]);         // whole match
console.log(m.indices[1], m.indices[2]);   // group 1, group 2
console.log(m.indices.groups.y);           // the same pair, by name
2026-10 [5, 12] [5, 9] [10, 12] [5, 9]

// Group Modifiers: (?flags:...)

RegExp Flags showed how to turn a flag on for one group with (?i:...). A minus sign turns a flag off for one group instead:

const re = /(?-i:W3)schools/i;   // i everywhere EXCEPT inside the group

console.log(re.test("W3SCHOOLS"));   // true: "schools" ignores case
console.log(re.test("w3SCHOOLS"));   // false: "W3" must match exactly
true false

Only i, m, and s can be switched this way. Any other letter is an error: /(?g:a)/ is SyntaxError: Invalid regular expression: /(?g:a)/: Invalid group.

Lookarounds Are Not Capturing Groups
W3Schools lists (?=x) and (?<=x) in its groups table because they use parentheses. They are assertions (RegExp Assertions): they check what is around the match and capture nothing. "$5".match(/(?<=\$)(\d)/) is ["5", "5"], because the only group is (\d).

// Summary

(...)Group and capture (numbered)
(?<name>...)Group and capture with a name
(?:...)Group only, no capture
\1 \k<name>Backreference inside the pattern
$1 $<name>Group inside a replace() string
(?i:...) (?-i:...)Turn a flag on / off for one group

// Quick Check

What does this log?

console.log("aaa-bbb".match(/(\w)+-/)[1]);
Show answer
a. The group matches one character and the + repeats it three times, so group 1 keeps only the last repetition: the third "a".

You need parentheses only so + can repeat "ab", and you do not want to add a numbered group. Which pattern?

  1. /(ab)+/
  2. /(?:ab)+/
  3. /ab+/
  4. /[ab]+/
Show answer
B. A works but captures (adding group 1). C repeats only the b. D matches any mix of a and b, like "bba".

Fill in the blanks: to reuse group 1 inside the pattern you write ____; to insert group 1 in a replace() string you write ____.

Show answer
\1 inside the pattern (a backreference) and $1 in the replacement string. Named versions: \k<name> and $<name>.

// Try It

  1. Click Open Playground. Extract the hours and minutes from "Meet at 14:35" with two capturing groups
  2. Rewrite it with named groups and destructure .groups
  3. Swap "first last" names to "last, first" with replace and $1 / $2
  4. Use matchAll to list every #hashtag in a sentence without the #
// Video Walkthroughs
Last updated: Oct 10, 2026, 1:47 AM ET