completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Groups & backreferences

Capture regex groups, name them, reuse them in replacements, trace backreferences, handle alternatives, and highlight match ranges with indices.

By the end, you can
  • 01
    Read every slot in a matchFind match[0], each numbered group, each named group, and the undefined of a skipped group.
  • 02
    Use saved text againUse $1, $<name>, and replacer functions in replace(), and \1 and \k<name> inside a regex.
  • 03
    Pick the right kind of groupGroup choices with |, skip saving when you don't need it, check browser support, and find where each group is with the d flag.

Save pieces of a match and use them again

A regular expression, or regex, is a pattern for finding text. So far, a regex told you two things: did it match, and what text did it match. This lesson shows how a regex can also save smaller pieces of that text. Then you can use those pieces again.

The main idea

A capturing group is a part of a regex inside round brackets, like (\d{4}). When the regex matches, the group saves the text that its part matched. This saved text is called a capture.

You can use a capture again in three ways:

  • Read it from the match result, like match[1].
  • Put it into new text with replace(), using a token like $1 or $<name>. A token is a short code that gets swapped for the saved text.
  • Ask for the same text again, later in the same regex. This is a backreference, like \1 or \k<name>.

This table lists the four kinds of round-bracket groups in this lesson. Read each row from left to right: the name, how you write it, what it does, and when to use it.

Four kinds of groups
NameHow you write itWhat it doesWhen to use it
Capturing group(...)Saves the text it matched in match[1], match[2], and so on.Split a date into year, month, and day.
Non-capturing group(?:...)Groups pieces together, but saves nothing and takes no number.Use ?, +, or | on several pieces at once.
Named group(?<year>...)Saves the text by number and by name, in match.groups.year.Read parts by clear names, and use $<year> in replacements.
Lookaround(?=...), (?<=...)Checks the text next to a spot, without making it part of the match.You will learn it in the next lesson.

This lesson is only about groups. Other lessons cover the rest:

  • How to make a regex, flags, and the methods test, exec, match, matchAll, and replace: Patterns & flags.
  • Repeat marks like + and *: Quantifiers.
  • Copying the properties of an object into variables, which helps with named groups: Destructuring.
Real-life analogyA form with labeled boxes

Picture a paper form for a shipping date. It has three boxes: year, month, and day. Someone reads a date and writes each part in its own box. Later you can read any box on its own. You can copy a box onto another form. You can also check that the order number is written the same way in two places.

In real life: The form has a box for year, month, and day
In JavaScript: The regex has a group for year, month, and day
In real life: The boxes are numbered 1, 2, 3 from left to right
In JavaScript: The groups fill match[1], match[2], and match[3] in order
In real life: A box with a printed label, like Year, is easier to find than Box 1
In JavaScript: A named group like (?<year>...) is easier to read than $1
In real life: Check that the order number is written the same way twice
In JavaScript: A backreference asks for the same text again

Numbered groups: how exec() fills the result

STEP THROUGH

Groups get numbers in the order of their opening round bracket, (, counted from left to right. In /(\d{4})-(\d{2})-(\d{2})/:

  • the year, (\d{4}), is group 1;
  • the month is group 2;
  • the day is group 3.

exec() runs the regex on some text and gives back a match result. The match result is a list. match[0] holds the whole match. match[1], match[2], and so on hold the groups.

The pattern (\d{4})-(\d{2})-(\d{2}) matching 2026-09-26. Group 1 saves 2026 in match[1]. Group 2 saves 09 in match[2]. Group 3 saves 26 in match[3]. The hyphens are not groups, so they get no slot.(\d{4})-(\d{2})-(\d{2})group 1group 2group 32026match[1]09match[2]26match[3]
Each pair of round brackets saves its piece of 2026-09-26 in a numbered slot. The hyphens have no brackets, so they get no slot.
What exec() gives backPop out in the code editor (opens in a new tab)JavaScript
const match = /(tea) (milk)/.exec("buy tea milk");console.log(match[0], match[1], match[2]);console.log(match.index);console.log(match.groups);

Line 1 runs exec() on "buy tea milk". The regex has two groups, (tea) and (milk). Line 2 prints tea milk tea milk: first the whole match, then group 1, then group 2.

Line 3 prints 4. match.index is the position where the match starts. Positions count from 0, so the t of tea is at 4. Line 4 prints undefined. match.groups holds named groups, and this regex has none.

Watch exec() fill the numbered slots
Step 0 of 7Ready
Your turn: follow the blue line

Step through exec() and watch the groups fill the match result.

Running in
  1. script
Next: line 1
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]);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick the text to search

When you pick other text, the step-through starts again. The group numbers stay the same, because the regex did not change.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Step through the code above. The first ( on line 1 starts the year group. So after exec() finds a match, match[1] is "2026". The second ( starts match[2], and the third starts match[3]. The hyphens and the digit codes like \d{2} get no slots. Only round brackets make groups.

A group that gets skippedPop out in the code editor (opens in a new tab)JavaScript
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 ? at the end of line 1 makes the -(draft) part optional. (The ?: makes a group that saves nothing. You will meet it later in this lesson.) So the (draft) group can be used or skipped.

  • Line 2 prints 42, the number that group 1 saved.
  • Line 3 prints undefined. The text file-42 has no -draft, so the (draft) group was skipped.
  • Line 4 prints draft, because file-42-draft has it.

A skipped group is not the same as a failed match. If the whole regex fails, exec() returns null, and there are no slots at all.

A skipped group or an empty capturePop out in the code editor (opens in a new tab)JavaScript
const skipped = /(tea)?milk/.exec("milk");const empty = /(te*)/.exec("te");console.log(skipped[1]);console.log(empty[1]);console.log(/((?:tea)*)/.exec("")[1]);

Line 3 prints undefined. The text is only milk, so the optional (tea) group was skipped. Line 4 prints te, the two letters the group saved. Line 5 prints an empty string, "". The outer group was used, but the * inside it repeated zero times, so it saved no letters.

So undefined means the group was skipped. An empty string means the group was used but saved nothing.

Named groups and replacements

REAL OUTPUT

A named group has a name as well as a number. You write it as (?<name>...). The match result then gets a groups object, and each name is a property on it, like match.groups.year.

Names make long regexes easier to read. You can also use destructuring to copy the names you need into variables: const { year } = match.groups.

Read named groups into variablesPop out in the code editor (opens in a new tab)JavaScript
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);console.log(match[1], match.groups.year);

Line 1 names three groups: year, month, and day. Line 2 copies them out of match.groups into three variables. Line 3 prints 2026. Line 4 puts the parts in a new order and prints 26/09/2026. You don't need to remember that the day was group 3.

Line 5 prints 2026 2026. A named group still has a number too, so match[1] and match.groups.year hold the same text.

Get the groups from every match with matchAll()Pop out in the code editor (opens in a new tab)JavaScript
const text = "Ship 2026-09-26, return 2027-01-05";const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;const dates = [...text.matchAll(pattern)].map(({ groups }) =>  groups.month + "/" + groups.day + "/" + groups.year,);console.log(dates.join(", "));

exec() gives you one match. matchAll() gives you every match, each with its own groups. It needs the g flag, which means "find all matches".

  • Line 2 adds g, so both dates are found.
  • On line 3, ({ groups }) takes the groups object out of each match.
  • Lines 3 to 5 turn each groups object into a date like 09/26/2026.
  • Line 6 prints 09/26/2026, 01/05/2027.

Plain match() with g is different. It gives only the matched text of each match, without the groups.

Use matchAll() once, and always with g

matchAll() gives back an iterator: a list you can read only once, from start to end. Spread it into an array with [...], or loop over it once. If the regex has no g flag, matchAll() throws a TypeError.

Groups also work in replace(). In a replacement string, $1 means "the text group 1 saved". $2 means group 2, and so on. So "$2/$3/$1" writes the month, then the day, then the year.

The replacement string "$2/$3/$1". $2 becomes 09, the month. $3 becomes 26, the day. $1 becomes 2026, the year. The slashes stay as they are. The result is 09/26/2026.iso.replace(pattern, "$2/$3/$1")replacement$2/$3/$1result09/26/2026
Each $ number is swapped for the text that group saved. The slashes are plain text, so they stay.
Three ways to use groups in replace()
Step 0 of 7Ready
Your turn: follow the blue line

Step through three ways to use groups in replace(): $1, $<name>, and a replacer function.

Running in
  1. script
Next: line 1
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;}));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Special codes in a replacement stringPop out in the code editor (opens in a new tab)JavaScript
const text = "tea milk";console.log(text.replace(/(tea) (milk)/, "$2 $1"));console.log(text.replace(/(tea) (milk)/, "$$1 $2"));console.log(text.replace(/(tea) (milk)/, "$&"));
  • Line 2 prints milk tea. $2 puts in group 2, then $1 puts in group 1.
  • Line 3 prints $1 milk. $$ puts in one plain dollar sign, so the 1 after it is just text.
  • Line 4 prints tea milk, because $& puts in the whole match.

These $ codes only work in a replacement string. Inside the regex itself, you write \1 instead. You will see it in the backreferences section.

You can also pass a function to replace() instead of a string. This is called a replacer function. JavaScript calls it once for each match and uses what it returns. It gets these arguments, in this order:

  1. the whole match;
  2. the text of each group, one argument per group;
  3. the position where the match starts;
  4. the whole input text;
  5. the groups object, if the regex has named groups.

Use a function when the new text needs some work, such as a check or a change to capital letters.

Check for a match before you destructure

exec() returns null when there is no match. Reading .groups from null throws an error. In real code, check first: if (match?.groups) { const { year } = match.groups; }. The examples here skip the check, because their text always matches.

Groups that save nothing, and the | choice

SORT IT

Round brackets do two jobs at once:

  • They group pieces, so a mark after them works on the whole group. For example, | means "or", and + means "one or more".
  • They save the text, which uses up a slot number.

Sometimes you only need the first job. Then write (?:...). This is a non-capturing group: it groups, but it saves nothing and takes no number. Your other groups keep their numbers.

A saving group, a non-saving group, and a repeated groupPop out in the code editor (opens in a new tab)JavaScript
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]);
  • Line 1 has two capturing groups. (cat|dog) is group 1 and (food|toy) is group 2. Lines 3 and 4 print cat and food.
  • Line 2 uses (?:cat|dog). It groups the two animals but saves nothing. So (food|toy) becomes group 1, and line 5 prints food.
  • Line 6 shows one more rule. When a group repeats, it keeps only its last piece. /(\d)+/ matches 123, but group 1 holds only "3".

The next example uses two more marks. ^ means "the start of the text", and $ means "the end of the text".

The | splits the whole patternPop out in the code editor (opens in a new tab)JavaScript
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 loop on line 4 tests five words. Each printed line shows three results, in this order: loose, anchoredWrong, and anchoredRight.

The | splits the whole pattern into two halves. So ^cat|dog$ means "cat at the start" or "dog at the end". That is why catfish and hotdog give true in the middle result.

To mean "the whole text is exactly cat or dog", group the two words first: /^(?:cat|dog)$/. The next lesson covers ^ and $ in more depth.

Left: /^cat|dog$/ splits into ^cat or dog$, which means cat at the start or dog at the end. So hotdog passes and test gives true. Right: /^(?:cat|dog)$/ means the whole text is exactly cat or dog. So hotdog fails and test gives false./^cat|dog$/^catordog$cat at startdog at endhotdogtrue/^(?:cat|dog)$/^(?:cat|dog)$exactly cat or doghotdogfalse
Without a group, the | splits the whole pattern in two, so hotdog passes. The group (?:cat|dog) puts both words between ^ and $.
Which idea does each pattern use?
  • `/(\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>/`
Try it yourself
0 of 6 correct

Put each pattern under the idea that does the main work. Read the explanation after each one, even when you are right.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Backreferences: match the same text again

A backreference asks for the text a group saved, again, later in the same regex. It does not repeat the pattern. It repeats the text. If group 1 saved cat, the backreference must find cat. There are two ways to write one:

  • \1 points to group 1. In the same way, \2 points to group 2.
  • \k<name> points to a named group.

Backreferences can find a word typed twice. They can also check that a quote ends with the same quote mark it started with.

Find a word typed twice with \1Pop out in the code editor (opens in a new tab)JavaScript
const repeated = /(\w+)\s+\1/i;console.log(repeated.test("Tea tea"));console.log(repeated.test("Tea coffee"));const strict = /(\w+)\s+\1/;console.log(strict.test("Tea tea"));console.log(repeated.test("Tea teatime"));console.log(/\b(\w+)\s+\1\b/i.test("Tea teatime"));

In line 1, (\w+) saves one word. (\w is a letter, a digit, or _.) Next, \s+ means one or more spaces. Then \1 asks for the saved word again. The i flag at the end means "ignore case", so Tea and tea count as the same.

  • Line 2 prints true.
  • Line 3 prints false, because coffee is a different word.
  • Line 5 prints false. The strict regex on line 4 has no i flag, so Tea and tea are different.
  • Line 6 prints true for Tea teatime. The regex only needs tea at the start of the second word.
  • Line 7 adds \b, which means "the edge of a word". Now the second word must end right after tea, so line 7 prints false.
The pattern /(\w+)\s+\1/i. Row 1: in Tea tea, group 1 saves Tea, and \1 finds tea, the same word when case is ignored, so test gives true. Row 2: in Tea coffee, coffee is a different word, so test gives false./(\w+)\s+\1/iTeagroup 1same wordtea\1trueTeadifferent wordcoffeefalse
Group 1 saves a word. Then \1 asks for that same word again. The i flag ignores case, so Tea and tea count as the same.
Named backreferences: a repeated word and a pair of quotesPop out in the code editor (opens in a new tab)JavaScript
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'"));
  • Line 1 saves a word in a group named word. Then \k<word> asks for the same word after the spaces. Line 2 prints true.
  • Line 3 swaps the two words for one copy, $<word>, and prints Please stop.
  • Line 5 saves the opening quote mark, ' or ", in a group named quote. The \k<quote> at the end asks for that same mark to close it. Line 6 prints hi.
  • Line 7 prints false. The text "mismatch' starts with " but ends with '.
Backreferences make the search do more work

With a backreference, the regex must remember saved text while it searches. That is useful, but it costs time. Don't try to read big nested formats, like HTML, with one clever regex. A later lesson on regex speed shows when a pattern gets too slow.

The same group name in two choices

FEATURE DETECT

For a long time, JavaScript did not allow two groups with the same name. The ES2025 version of JavaScript allows it in one case: when the two groups are on different sides of a |. Only one side can match, so only one group with that name is ever used.

This helps when one date format puts year first and another puts year last.

This table shows where this works today. Read each row from left to right: what you try, whether it works, and what to do.

Where the same group name in two choices works
What you tryDoes it work?What to do
The same group name on both sides of |Yes, in modern browsers (ES2025).MDN lists Chrome 125+, Firefox 129+, and Safari 17+. The Chrome 154 used by this site supports it.
The same pattern in Node.js 22.23.1, which this project usesNo, not here.Building the regex throws SyntaxError: Duplicate capture group name. So the lesson keeps the pattern in a string and checks support first.
The same pattern in your own codeOnly where it is supported.Wrap new RegExp(source) in try/catch when not every browser or runtime supports a feature.
Does your browser allow the same group name twice?
Try the pattern and catch the errorPop out in the code editor (opens in a new tab)JavaScript
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);}
Your browserchecking
Statuschecking
Messagewaiting for browser
Try it yourself
Runs `new RegExp(source)` inside `try`/`catch`

The check runs in your browser after the page loads.

The pattern is kept in a string, so Node can still load this file. A browser without support throws only when new RegExp() runs, and catch handles that.

MDN's browser data says current Chrome, Firefox, and Safari support it. But this site's tests run on Node 22.23.1, and Node 22 still throws Duplicate capture group name. So this lesson does three things:

  • It keeps the pattern in a string, not in a regex written between slashes.
  • It builds the regex with new RegExp() inside try/catch, so an error can be caught.
  • It checks the browser result in a separate test.

Trying a feature and catching the error is called feature detection.

Match indices: where each group starts and ends

PLAYGROUND

The d flag adds an indices list to the match result. It tells you where each piece is in the text:

  • match.indices[0] is where the whole match starts and ends.
  • match.indices[1] is where group 1 starts and ends.
  • match.indices.groups.year is where the year group starts and ends.

Each one is a pair, [start, end]. The end is the position just after the last character. This is the same rule as slice(start, end).

A small d flag example
Where the year starts and endsPop out in the code editor (opens in a new tab)JavaScript
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);
Outputyear start and end

[8, 12]

Try it yourself
Real output

The d flag adds indices. Each pair is [start, end]. The end is the position just after the last character, the same as in slice(start, end).

A web page can use these pairs to highlight only the year, month, or day, not the whole match.
Print the start and end of each partPop out in the code editor (opens in a new tab)JavaScript
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(".."));
  • Line 3 prints 8..18. The date starts at position 8 and ends just before 18.
  • Line 4 prints 8..12 for the year.
  • Lines 5 and 6 print 13..15 and 16..18 for the month and the day.

These numbers are positions in the original text. But a position is not always the number of characters you see on screen. The next example shows why.

Positions count code units, not the characters you seePop out in the code editor (opens in a new tab)JavaScript
const text = "\u{1D11E} tea";const match = /(?<item>tea)/d.exec(text);console.log(match.indices.groups.item.join(".."));console.log(text.slice(...match.indices.groups.item));

Line 1 starts the text with a music symbol, written as \u{1D11E}. You see one symbol. But JavaScript stores text in small pieces called UTF-16 code units, and this symbol takes two of them. Then comes a space. So tea starts at position 3, not 2.

Line 3 prints 3..6. Line 4 passes the same pair to slice() and prints tea. So pass the positions straight to slice(). Don't count the characters you see.

Highlight each group with the d flag
The code the playground runsPop out in the code editor (opens in a new tab)JavaScript
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);}
Where each group isd

Created 20261: year-092: month-263: day by Ada

  1. Group 1 · year "2026" 8..12
  2. Group 2 · month "09" 13..15
  3. Group 3 · day "26" 16..18
Try it yourself

The whole match is 2026-09-26. Each highlight uses a start and end from match.indices, so it covers exactly the right characters.

The text and the pattern are limited to 80 characters. The pattern is built inside try/catch and runs with the d flag, so each group gives its start and end.

This is how a regex can help a web page. A log viewer can save the time, level, and message of each line in groups. Then it can highlight exactly those positions, without guessing lengths. The playground above limits how long the text and the pattern can be. It also catches a broken pattern, so your edits can't crash the page.

Where groups and backreferences show up

Groups help most when you do something with the pieces after the match. If you only need a yes or no, a regex without groups is often enough. Groups are worth it when you want to read, replace, highlight, or compare the pieces.

Each row of this table is a real task. The second column says how groups help with it.

Real tasks that use groups
TaskHow groups help
Reading datesSave the year, month, and day. Then check them or write them in a new order.
Log linesSave the time, level, and message. Leave out the marks between them.
Formatting namesTurn Last, First into First Last with named tokens or a replacer function.
Cleaning up text in an editorFind a word typed twice with a backreference, and remove the extra copy.
Highlighting on a pageUse match.indices.groups to mark exactly where each group is in the text.

A log line is one line of notes that a program writes about what it did. The next example reads two parts of one.

Read the parts of a log linePop out in the code editor (opens in a new tab)JavaScript
const line = "INFO: user=Asha";const pattern = /(?<level>[A-Z]+): user=(?<user>\w+)/;const match = pattern.exec(line);console.log(match.groups.level);console.log(match.groups.user);

Line 2 names two groups: level and user. Line 3 runs the regex. Lines 4 and 5 read the two groups by name and print INFO and Asha.

For a full log format with many rules, use a parser instead of one big regex. A parser is a program that reads a format step by step.

Turn each item into a price labelPop out in the code editor (opens in a new tab)JavaScript
const cart = "tea: 3, milk: 5";const item = /(?<name>\w+): (?<price>\d+)/g;const labels = cart.replace(item, (full, name, price) => name + " costs $" + price);console.log(labels);

Line 2 uses the g flag, so replace() changes both items. Line 3 passes a replacer function. For each match, it gets the whole match, then the name group, then the price group. It returns a new label. Line 4 prints tea costs $3, milk costs $5. Use a function like this when the new text needs to be built, not just copied.

Three more places where groups help:

  • Checking a whole form field: exec() also finds matches in the middle of longer text. Put ^ and $ around the whole pattern when extra characters are not allowed.
  • Cleaning up text: a backreference can find a word typed twice. Then an editor can turn the the into the.
  • Highlighting: indices.groups tells a web page exactly where the year, month, day, or quoted text is.

Common misconceptions

  • "Round brackets always mean I want the text later." Not always. Use (?:...) when you only need to group, for | or a repeat mark like +.
  • "Group numbers follow how close the groups look." Numbers follow the order of the opening (, from left to right. This is true even for a group inside another group, and for a group that gets skipped.
  • "A skipped optional group gives an empty string." It gives undefined. An empty string means the group was used but saved no characters.
  • "^cat|dog$ means exactly cat or dog." The | splits the whole pattern into ^cat or dog$. Write ^(?:cat|dog)$ for exact words.
  • "A backreference repeats the pattern." It repeats the saved text. If group 1 saved "cat", \1 must find "cat".
  • "If browsers support a regex feature, Node does too." The same group name in two choices works in modern browsers. This project's Node 22 still rejects it.

This table compares four kinds of groups. Read down a column to see how one kind behaves.

Four kinds of groups side by side
QuestionCapturing (...)Non-capturing (?:...)Named (?<name>...)Lookaround (next lesson)
Is its text part of the match?Yes.Yes.Yes.No. It only checks the text next to a spot.
Does it save text in a slot?Yes, in a numbered slot.No. It saves nothing.Yes, in a numbered slot and under its name.No. It saves nothing.
When should you use it?When you need the text later.When you only need to group.When you want parts with clear names.When you need to check nearby text without taking it.
Where is it taught?In this lesson.In this lesson.In this lesson.In the next lesson.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upRead a numbered group

What does this code print? Type the exact value.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const match = /(\d{4})-(\d{2})-(\d{2})/.exec("2026-09-26");
console.log(match[2]);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upSpot a skipped group

    Type the value that the code prints for the optional group.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const match = /file-(\d+)(?:-(draft))?/.exec("file-42");
    console.log(match[2]);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWrite the replacement string

      Replace TODO with the replacement string that makes the code print 09/26/2026.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const iso = "2026-09-26";
      console.log(iso.replace(/(\d{4})-(\d{2})-(\d{2})/, "TODO"));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix the cat-or-dog bug

        Write a regex that matches exactly cat or exactly dog, and nothing longer.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const pattern = /^cat|dog$/;
        console.log(pattern.test("catfish"));
        console.log(pattern.test("hotdog"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeMake the closing quote match the opening one

          What should replace TODO after .*?, so the text must end with the same quote mark it started with?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const sameQuote = /(?<quote>['"])(?<text>.*?)TODO/;
          console.log(sameQuote.test("\"ok\""));
          console.log(sameQuote.test("\"broken'"));

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS

            Some questions ask what a word means. Others show code and ask what it prints. For code questions, first number the groups. Then work out the last line.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8How do capturing groups get their numbers?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does the month code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const match = /(\d{4})-(\d{2})-(\d{2})/.exec("2026-09-26");
              console.log(match[2]);

              Choose an answer to see the explanation.

            3. Question 3 of 8What does this named-group code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const 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.

            4. Question 4 of 8What does a skipped optional group give?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const match = /file-(\d+)(?:-(draft))?/.exec("file-42");
              console.log(match[2]);

              Choose an answer to see the explanation.

            5. Question 5 of 8Why use (?:...)?

              Choose an answer to see the explanation.

            6. Question 6 of 8What is wrong with /^cat|dog$/ if you want exactly cat or dog?

              Choose an answer to see the explanation.

            7. Question 7 of 8What does the repeated-word check print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const 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.

            8. Question 8 of 8Which sentence about using the same group name twice is true for this project?

              Choose an answer to see the explanation.

            Key takeaways

            • Groups get numbers in the order of their opening (. match[0] is the whole match.
            • A skipped group gives undefined. A repeated group keeps only its last piece.
            • Named groups make match results, destructuring, and replacements easier to read.
            • Use (?:...) to group without saving and without changing the numbers.
            • A backreference asks for the same saved text again. The d flag tells you where each piece starts and ends.

            Remember the one-liner.
            Groups save pieces of a match. Backreferences and replacements use those saved pieces again.

            Up next: Anchors, boundaries & lookaround. There you match spots in the text, like the start of the text or the edge of a word. You also check the text next to a spot without making it part of the match.

            CompleteFrontend Clear concepts. Working examples.