Groups & backreferences
Capture regex groups, name them, reuse them in replacements, trace backreferences, handle alternatives, and highlight match ranges with indices.
- 01Read every capture slotTrace how
exec()fillsmatch[0], numbered captures, named groups, and missing groups. - 02Reuse matched textUse
$1,$<name>, replacer callbacks,\1, and\k<name>without losing track of intent. - 03Choose the right grouping toolGroup alternatives, avoid accidental captures, feature-detect duplicate names, and highlight exact ranges with the
dflag.
Capture and reuse pieces of a match
A regular expression can do more than answer “does this string match?” Groups let a pattern remember the exact pieces it matched. Backreferences then ask the pattern, or a replacement, to reuse those pieces.
A capturing group is parentheses around part of a regex, like (\d{4}). It stores the text that part matched. A backreference, like \1 or \k<name>, matches the same text again or reuses it in a replacement.
| Tool | Syntax | What it does | Good use |
|---|---|---|---|
| Capturing group | (...) | Stores the text it matched in match[1], match[2], and so on. | Parsing a date into year, month, day. |
| Non-capturing group | (?:...) | Groups pattern pieces without creating a capture slot. | Applying ?, +, or | to a subpattern. |
| Named group | (?<year>...) | Stores the text by number and by name in match.groups.year. | Readable parsing and $<year> replacements. |
| Lookaround preview | (?=...), (?<=...) | Checks nearby text without consuming or capturing it. | The next lesson covers anchors, boundaries, and lookaround. |
Stay in this lane: Patterns & flags covers creating regexes and methods like test, exec, match, matchAll, and basic replace. Quantifiers covers greediness and repetition. This lesson focuses on the parentheses, names, alternatives, and reuse. Named groups also pair nicely with destructuring.
Imagine scanning a shipping form. The date field has boxes. Once the boxes are filled, you can read “year,” “month,” and “day,” copy the order number somewhere else, or mark exactly which characters belong to each box.
- In real life: A date line has boxes for year, month, and day
- In JavaScript: A regex has groups for year, month, and day
- In real life: The box order gives you field numbers
- In JavaScript: Opening parentheses give you
match[1],match[2], andmatch[3] - In real life: A label like Year is easier than Box 1
- In JavaScript:
(?<year>...)is easier than remembering$1 - In real life: Copy the same order number twice
- In JavaScript: A backreference requires the same text again
Where the analogy stops: A paper form accepts whatever someone writes in each box. A regex group only stores text that the pattern actually matched, and repeated groups keep only their last captured text.
Capturing groups: how exec() fills the array
STEP THROUGHNumbered groups are assigned by the order of their opening parenthesis. In /(\d{4})-(\d{2})-(\d{2})/, the year is group 1, the month is group 2, and the day is group 3. match[0] is different: it is the entire match.
Step through exec() and watch how capture groups fill the match array.
script
const text = "date: 2026-09-26";const match = pattern.exec(text);console.log(match[0]);console.log(match[1]);console.log(match[2]);console.log(match[3]);Read line 1 carefully. The first ( opens the year group, so after exec() succeeds, match[1] is "2026". The second opening parenthesis becomes match[2], and the third becomes match[3]. Hyphens and character classes do not get slots because they are not capturing groups.
const pattern = /file-(\d+)(?:-(draft))?/;console.log(pattern.exec("file-42")[1]);console.log(pattern.exec("file-42")[2]);console.log(pattern.exec("file-42-draft")[2]);The optional (draft) group exists in the pattern. For file-42, that path did not run, so its array slot is undefined. That is different from the whole pattern failing, which would make exec() return null.
Named groups and replacements
REAL OUTPUTNamed groups still get numbered slots, but they also appear on match.groups. Names make longer patterns readable, and object destructuring lets you pull out only the pieces you need.
const match = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.exec("2026-09-26");const { year, month, day } = match.groups;console.log(year);console.log(day + "/" + month + "/" + year);Line 1 names three captures. Line 2 destructures match.groups, so the final log can say day + "/" + month + "/" + year instead of remembering that day was group 3.
Step through the three replacement styles: $1, $<name>, and a replacer function with a groups object.
script
console.log(iso.replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1")); const person = "Lovelace, Ada";const namePattern = /(?<last>\w+), (?<first>\w+)/;console.log(person.replace(namePattern, "$<first> $<last>"));console.log(person.replace(namePattern, (full, last, first, offset, input, groups) => { return groups.first.toUpperCase() + " " + groups.last.toUpperCase() + " at " + offset;}));Replacement strings use $1, $2, and $<name>. A replacer function receives more information: the full match, each capture, the match offset, the whole input string, and finally the groups object when named groups exist. Use that function when the replacement needs computation, validation, or case changes.
exec() can return null. In production, check the match before destructuring: if (match?.groups) { const { year } = match.groups; }. The examples use known matching input so the main idea stays visible.
Non-capturing groups and alternation precedence
SORT ITParentheses have two jobs: group syntax and, by default, capture text. When you only need the grouping job, write (?:...). It keeps match[1] reserved for meaningful data and avoids accidental shifts in numbering.
const capturing = /^(cat|dog)-(food|toy)$/;const nonCapturing = /^(?:cat|dog)-(food|toy)$/;console.log(capturing.exec("cat-food")[1]);console.log(capturing.exec("cat-food")[2]);console.log(nonCapturing.exec("cat-food")[1]);console.log(/(\d)+/.exec("123")[1]);In the capturing pattern, (cat|dog) is group 1 and (food|toy) is group 2. In the non-capturing pattern, (?:cat|dog) groups the animal choices without storing them, so (food|toy) becomes group 1. The last line shows another edge: a quantified group keeps only the last repetition, so /(\d)+/ captures "3" after matching "123".
const loose = /cat|dog/;const anchoredWrong = /^cat|dog$/;const anchoredRight = /^(?:cat|dog)$/;for (const word of ["cat", "dog", "catfish", "hotdog", "bird"]) { console.log(word + ": " + loose.test(word) + " " + anchoredWrong.test(word) + " " + anchoredRight.test(word));}The pipe is low-precedence: ^cat|dog$ means “starts with cat” or “ends with dog.” If you meant the whole string must be either word, group the alternatives: /^(?:cat|dog)$/. The next lesson covers anchors in depth; here the key is that grouping decides what the anchors apply to.
`/(\d{4})-(\d{2})-(\d{2})/``/^(?:cat|dog)$/``/\b(?<word>\w+)\s+\k<word>\b/i``/cat|dog/``text.replace(pattern, "$<first> $<last>")``/(?<quote>['"]).*?\k<quote>/`
Sort each snippet by the idea doing the important work. Read the explanation even when you guess correctly.
Backreferences: match the same text again
A backreference is stricter than “the same pattern again.” It means “the exact text this group captured.” \1 points to numbered group 1. \k<name> points to a named group. That makes backreferences great for duplicate-word checks and matching paired quotes.
const repeated = /\b(?<word>[A-Za-z]+)\s+\k<word>\b/i;console.log(repeated.test("Please please stop"));console.log("Please please stop".replace(repeated, "$<word>")); const quoted = /(?<quote>['"])(?<text>.*?)\k<quote>/;console.log(quoted.exec("say \"hi\" and 'bye'").groups.text);console.log(quoted.test("\"mismatch'"));The repeated-word pattern captures a word, then \k<word> requires the same word after whitespace. The quote pattern captures either ' or " as quote, then the closing delimiter must be the same character. That is why "mismatch' fails.
Backreferences add memory to a regex search. They are useful, but avoid using them as a general parser for nested languages. The later regex performance lesson explains when a clever pattern becomes too expensive.
Duplicate named groups in different alternatives
FEATURE DETECTJavaScript historically rejected two groups with the same name. ES2025 relaxes that for groups in different alternatives when only one can participate. For example, one date format might put year first and another might put year last.
| Feature | Status | What to do |
|---|---|---|
| Duplicate names in different alternatives | ES2025 / modern browsers | MDN compatibility data lists Chrome 125+, Firefox 129+, and Safari 17+. The Chrome 154 used by this site supports it. |
| Node.js 22.23.1 in this project | Not supported here | Constructing the pattern throws SyntaxError: Duplicate capture group name, so lesson modules keep the example as a string and feature-detect it. |
| Safe production habit | Feature detect | Use try { new RegExp(source) } when a pattern uses a feature that not every runtime parses. |
const source = "(?<year>\\d{4})-\\d{2}|\\d{2}/(?<year>\\d{4})";try { const pattern = new RegExp(source); console.log(pattern.exec("2026-09").groups.year); console.log(pattern.exec("09/2026").groups.year);} catch (error) { console.log(error.name + ": " + error.message);}checkingwaiting for browserThe panel feature-detects support after the page loads so server rendering and browser hydration agree.
MDN’s compatibility data lists support in current Chrome, Firefox, and Safari. This repository’s CI and tests run on Node 22.23.1, whose V8 still throws Duplicate capture group name. That is why this lesson keeps the source in a string, constructs it inside try/catch, and proves the browser output separately.
Match indices: highlight exact captured ranges
PLAYGROUNDThe d flag adds range data to the match object. match.indices[0] is the full match range. match.indices[1] is capture 1, and match.indices.groups.year is the named group’s range. Each pair is [start, end], where end is exclusive, just like slice(start, end).
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/d;const match = pattern.exec("Created 2026-09-26");console.log(match.indices.groups.year);[8, 12]
The d flag adds indices. Each pair is [start, end], where end is exclusive, exactly like slice(start, end).
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/d;const match = pattern.exec("Ship on 2026-09-26.");console.log(match.indices[0].join(".."));console.log(match.indices.groups.year.join(".."));console.log(match.indices.groups.month.join(".."));console.log(match.indices.groups.day.join(".."));try { const pattern = new RegExp("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})", "d"); const match = pattern.exec("Created 2026-09-26 by Ada"); console.log(match?.indices?.groups ?? match?.indices ?? "no match");} catch (error) { console.log(error.name + ": " + error.message);}Created 20261: year-092: month-263: day by Ada
- Group 1 · year
"2026"8..12 - Group 2 · month
"09"13..15 - Group 3 · day
"26"16..18
The full match is 2026-09-26. The highlighted chips use ranges from match.indices, so they line up with the exact characters.
try/catch, and run with the d flag so captures expose start and end indices.This is the bridge from regex to UI. A log viewer can capture the timestamp, level, and message, then highlight exactly those ranges without guessing string lengths. The playground bounds the input and catches invalid patterns so a learner’s edit cannot crash the page.
Where groups and backreferences show up
Groups are most useful when the pieces have a job after the match. If you only need a yes or no, a simpler pattern may be enough. If you will parse, replace, highlight, or validate relationships between pieces, captures earn their place.
| Use case | How groups help |
|---|---|
| Parsing dates | Capture year, month, and day, then validate or reformat them. |
| Log lines | Capture a timestamp, level, and message while keeping separators out of the result. |
| Name formatting | Turn Last, First into First Last with named replacements or a replacer callback. |
| Text editor cleanup | Find duplicate words with a backreference and replace the repeated copy. |
| UI highlighting | Use match.indices.groups to mark exactly which substring each capture produced. |
- Parsing dates and log lines: named groups make later code read like data fields instead of slot numbers.
- Reformatting names:
$<first> $<last>turnsLast, FirstintoFirst Lastwithout a manual split. - Swapping and cleanup: duplicate-word detection can power editor fixes like replacing
the thewiththe. - Highlighting:
indices.groupslets a UI mark the precise year, month, day, or quoted text that the regex captured.
Common misconceptions
- “Parentheses always mean I want the text later.” Use
(?:...)when you only need grouping for alternation or quantifiers. - “Group numbers follow visual closeness.” They follow opening parenthesis order, even when groups are nested or alternatives skip some groups.
- “An unmatched optional group is an empty string.” It is
undefined. Empty string means the group participated and matched nothing. - “
cat|dogis automatically anchored as a unit.” The pipe splits alternatives at low precedence. Use^(?:cat|dog)$for exact words. - “A backreference repeats the pattern.” It repeats the captured text. If group 1 captured
"cat",\1must match"cat". - “Browser support means Node support.” Duplicate named groups in alternatives are supported in modern browsers but still rejected by this project’s Node 22.
| Question | Capturing | Non-capturing | Named | Lookaround preview |
|---|---|---|---|---|
| Does it consume characters? | Yes | Yes | Yes | No, it checks a position |
| Does it create a capture slot? | Yes | No | Yes, plus a name | No |
| Best reason to use it | You need the text later | You only need grouping | You want readable fields | You need context without including it |
| Lesson scope | This lesson | This lesson | This lesson | Next lesson |
Practice exercises
5 EXERCISESPredict the exact value logged by the code.
const match = /(\d{4})-(\d{2})-(\d{2})/.exec("2026-09-26");
console.log(match[2]);The second opening parenthesis is the month group, so match[2] prints 09.
Type the value printed for the optional group.
const match = /file-(\d+)(?:-(draft))?/.exec("file-42");
console.log(match[2]);The optional group exists, but it did not participate for file-42, so JavaScript stores undefined in that slot.
Replace TODO with the replacement string that prints 09/26/2026.
const iso = "2026-09-26";
console.log(iso.replace(/(\d{4})-(\d{2})-(\d{2})/, "TODO"));const iso = "2026-09-26";
console.log(iso.replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1"));$2/$3/$1 reuses the captured month, day, and year in the new order.
Write a regex that matches exactly cat or exactly dog, but not longer words.
const pattern = /^cat|dog$/;
console.log(pattern.test("catfish"));
console.log(pattern.test("hotdog"));const pattern = /^(?:cat|dog)$/;
console.log(pattern.test("catfish"));
console.log(pattern.test("hotdog"));/^(?:cat|dog)$/ applies both anchors to the grouped alternatives, so catfish and hotdog are false.
If the pattern had a TODO after .*?, what backreference would require the same quote at the end?
const sameQuote = /(?<quote>['"])(?<text>.*?)TODO/;
console.log(sameQuote.test("\"ok\""));
console.log(sameQuote.test("\"broken'"));const sameQuote = /(?<quote>['"])(?<text>.*?)\k<quote>/;
console.log(sameQuote.test("\"ok\""));
console.log(sameQuote.test("\"broken'"));The pattern uses \k<quote> so the closing delimiter must be the same quote character that opened the string.
Check your understanding
8 QUESTIONSThese questions mix vocabulary with real outputs. For code questions, identify the groups first, then run the final line in your head.
Question 1 of 8How are numbered capturing groups assigned?
Choose an answer to see the explanation.
Question 2 of 8What does the month code print?
Read the code, then predictconst match = /(\d{4})-(\d{2})-(\d{2})/.exec("2026-09-26"); console.log(match[2]);Choose an answer to see the explanation.
Question 3 of 8What does the named-group destructuring code print on its second line?
Read the code, then predictconst match = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.exec("2026-09-26"); const { year, month, day } = match.groups; console.log(day + "/" + month + "/" + year);Choose an answer to see the explanation.
Question 4 of 8What value does an optional group produce when it does not participate?
Read the code, then predictconst match = /file-(\d+)(?:-(draft))?/.exec("file-42"); console.log(match[2]);Choose an answer to see the explanation.
Question 5 of 8Why use
(?:...)?Choose an answer to see the explanation.
Question 6 of 8What is wrong with
/^cat|dog$/when you meant exactly cat or dog?Choose an answer to see the explanation.
Question 7 of 8What does the repeated-word check print?
Read the code, then predictconst repeated = /\b(?<word>[A-Za-z]+)\s+\k<word>\b/i; console.log(repeated.test("Please please stop"));Choose an answer to see the explanation.
Question 8 of 8Which statement about duplicate named groups in alternatives is accurate for this project?
Choose an answer to see the explanation.
Key takeaways
- Capturing groups fill slots by opening-parenthesis order;
match[0]is the full match. - Skipped groups are
undefined; repeated quantified groups keep only the last capture. - Named groups make matches, destructuring, and replacements easier to read.
- Use
(?:...)for grouping without shifting capture numbers. - Backreferences match the same captured text, and
dflag indices give exact UI ranges.
Remember the one-liner.
Groups save pieces of a regex match; backreferences and replacements reuse those saved pieces.
Up next: Anchors, boundaries & lookaround, where you match positions like the start of a string, word edges, and nearby context without capturing it.