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."
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
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})/);
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);
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}`);
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.
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"
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"
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"
// Named version: \k<name>
/(?<q>["']).*?\k<q>/ // a quote, text, then the SAME kind of quote
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]"));
(\w)+ captures one character at a time and keeps the final one. To capture the whole run, put the quantifier inside: (\w+).? is undefined when it did not match (in a replace string, $1 just becomes empty). Check with === undefined before calling string methods on it.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));
}
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
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
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.
(?=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).
| (...) | 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 |
What does this log?
console.log("aaa-bbb".match(/(\w)+-/)[1]);
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?
/(ab)+//(?:ab)+//ab+//[ab]+/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 ____.
\1 inside the pattern (a backreference) and $1 in the replacement string. Named versions: \k<name> and $<name>."Meet at 14:35" with two capturing groups.groupsreplace and $1 / $2matchAll to list every #hashtag in a sentence without the #