Groups & Capture
Groups capture and structure matches. (...) captures; (?:...) is non-capturing; (?<name>...) names a group. Backreferences (\1) reuse a captured value.
Capture, non-capture, named, backref
EXAMPLE
// 1) Capture groups
const m = 'order id=1234'.match(/id=(\d+)/);
m[0] // 'id=1234' — full match
m[1] // '1234' — first capture group
// 2) Multiple groups
const m2 = '2026-06-08'.match(/(\d{4})-(\d{2})-(\d{2})/);
m2[1] // '2026' (year)
m2[2] // '06' (month)
m2[3] // '08' (day)
// 3) Non-capturing — for grouping without storage
const m3 = 'hello world'.match(/(?:hello) (\w+)/);
m3[1] // 'world'
// (?:...) doesn't consume an index in the result
// 4) Named groups (ES2018+)
const m4 = '2026-06-08'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
m4.groups.year // '2026'
m4.groups.month // '06'
m4.groups.day // '08'
// Destructure
const { year, month, day } = m4.groups;
// 5) Alternation
const m5 = 'orange'.match(/(apple|orange|banana)/);
m5[1] // 'orange'
const m6 = '+61 412 345 678'.match(/^(\+?\d{1,3})\s?(\d{3})\s?(\d{3})\s?(\d{3})$/);
m6 // [full, '+61', '412', '345', '678']
// 6) Optional + repeating groups
'rgb(0, 128, 255)'.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
// → ['rgb(0, 128, 255)', '0', '128', '255']
// 7) Backreferences — refer to a previously-captured group
/(.)\1/.test('hello') // true — 'll' matches (.)(\1)
/^(['"])(.*?)\1$/.exec(`'single quoted'`)
// → matches the same quote char at start and end
// → ["'single quoted'", "'", 'single quoted']
// 8) Find duplicated words
const dupes = 'the the quick brown fox the'.match(/\b(\w+)\s+\1\b/);
dupes[1] // 'the'
// 9) Named backreference
/(?<quote>['"])(?<text>.*?)\k<quote>/.exec(`"hi"`);
// → ['"hi"', '"', 'hi']
// 10) Replace with captured groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1'); // 'Smith, John'
// Named groups in replace
'2026-06-08'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<day>/$<month>/$<year>',
);
// → '08/06/2026'
// 11) Replace with a function
'Hello, World'.replace(
/(\w+)/g,
(match, word) => word.toUpperCase(),
);
// → 'HELLO, WORLD'
// Named groups available via 4th+ arg (after groups + offset + string)
'2026-06-08'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
(_, year, month, day) => `${day}/${month}/${year}`,
);
// 12) matchAll — every match with capture groups
const text = 'id=1, id=2, id=3';
for (const m of text.matchAll(/id=(\d+)/g)) {
console.log(m[1]); // '1', '2', '3'
}
// matchAll requires the g flag
// 13) Real recipes
// 13a) Parse query string
for (const [, key, value] of 'a=1&b=2&c=hello'.matchAll(/([^=&]+)=([^&]*)/g)) {
console.log(key, decodeURIComponent(value));
}
// 13b) Extract code blocks from markdown
const markdown = '```python\\nprint(1)\\n```';
for (const m of markdown.matchAll(/```(\w+)\\n([\\s\\S]*?)```/g)) {
const language = m[1];
const code = m[2];
}
// 13c) Parse a URL
const url = 'https://user:pass@example.com:8080/path?q=1#frag';
const urlRegex = /^(?<scheme>[a-z]+):\/\/(?:(?<user>[^:]+):(?<pass>[^@@]+)@@)?(?<host>[^:\/?#]+)(?::(?<port>\d+))?(?<path>[^?#]*)(?:\?(?<query>[^#]*))?(?:#(?<frag>.*))?$/;
const { scheme, host, port, path } = url.match(urlRegex).groups;
// (Use new URL() in real code; this is for the example.)
// 13d) Extract HTML tag attributes
for (const m of '<a href="/x" title="link">x</a>'.matchAll(/(\w+)="([^"]*)"/g)) {
console.log(m[1], m[2]); // 'href', '/x' ; 'title', 'link'
}
// 14) Performance
// - Use non-capturing (?:...) when you don't need the value — saves work
// - Avoid deeply nested groups + alternation → catastrophic backtracking
// - Possessive quantifiers (in PCRE / Java / .NET) prevent backtracking; not in JS
// - Anchors (^ $ \b) prune the search; faster matches
// 15) Cross-language differences
// - JS: (?<name>) named, (?:) non-capture; modern engines have lookbehind
// - PCRE: (?P<name>) for Python compat; (?>...) atomic groups
// - Python (re): (?P<name>) named groups; (?P=name) backreference
// - .NET: (?<name>) and (?'name'); balancing groups for stack matching
// - Java: (?<name>) named
// 16) Common bugs
// • Forgetting non-capturing → group[1] is wrong, indices shift
// • Greedy ([\s\S]*) eating across multiple matches
// • Mixing named + numbered references in replace
// • Backreferences with the g flag — work, but each match resets state
// • Not anchoring (^ $) for validation regexes
// 17) When to reach for capturing groups
// - Extracting parts of a structured string (dates, URLs, key=value)
// - Replacing with parts of the original
// - Validating + extracting in one pass
// - Domain-specific parsers where a full parser is overkill
//
// Otherwise: non-capturing is cheaper.
Why it matters
Named groups ((?<year>\d{4})) make regex maintainable — the code reads like the data it’s extracting. Use non-capturing ((?:...)) wherever you’re grouping without needing the captured value.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
/(\d{4})-(\d{2})-(\d{2})/ // capture YMD
/(?:foo|bar)/ // non-capturing
/(?<year>\d{4})/ // named capture
Try it Yourself »
Exercise
Non-capturing group syntax.
/(?
foo|bar)/
One character.
Discussion
Loading…