cf.completefrontendCode editorOpen lab
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 capture slotTrace how exec() fills match[0], numbered captures, named groups, and missing groups.
  • 02
    Reuse matched textUse $1, $<name>, replacer callbacks, \1, and \k<name> without losing track of intent.
  • 03
    Choose the right grouping toolGroup alternatives, avoid accidental captures, feature-detect duplicate names, and highlight exact ranges with the d flag.

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.

Definition

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.

Four grouping tools you will compare
ToolSyntaxWhat it doesGood 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.

Real-life analogyA form with labeled boxes

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], and match[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 THROUGH

Numbered 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.

Numbered groups in exec()
Step 0 of 7Ready
Your turn: follow the blue line

Step through exec() and watch how capture groups fill the match array.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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]);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the input string

Changing the input starts a fresh replay. The group numbers stay the same because the pattern 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.

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.

A group can exist but not participatePop 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 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 OUTPUT

Named 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.

Named groups plus destructuringPop 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);

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.

Replacement tokens and replacer arguments
Step 0 of 7Ready
Your turn: follow the blue line

Step through the three replacement styles: $1, $<name>, and a replacer function with a groups object.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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;}));
CallStoreChangeResultRun = next line. 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.

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.

Destructure only after checking for a match

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 IT

Parentheses 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.

Capturing, non-capturing, and repeated capturesPop 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]);

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".

Alternation has low precedencePop 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 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.

Which grouping idea is this?
  • `/(\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

Sort each snippet by the idea doing the important work. Read the explanation even when you guess correctly.

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 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.

Duplicate words and matching quote pairsPop 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'"));

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 can make patterns heavier

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 DETECT

JavaScript 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.

Support for duplicate named groups in disjoint alternatives
FeatureStatusWhat to do
Duplicate names in different alternativesES2025 / modern browsersMDN 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 projectNot supported hereConstructing the pattern throws SyntaxError: Duplicate capture group name, so lesson modules keep the example as a string and feature-detect it.
Safe production habitFeature detectUse try { new RegExp(source) } when a pattern uses a feature that not every runtime parses.
Feature-detect duplicate named groups
Duplicate named group feature detectionPop 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);}
Current runtimechecking
Statuschecking
Messagewaiting for browser
Try it yourself
Runs `new RegExp(source)` inside `try`/`catch`

The panel feature-detects support after the page loads so server rendering and browser hydration agree.

The same source is safe to import in Node because it is a string. Unsupported engines reject it at construction time, not module parse time.

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

PLAYGROUND

The 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).

Minimal d flag example
Indices starterPop 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 range

[8, 12]

Try it yourself
Real range data

The d flag adds indices. Each pair is [start, end], where end is exclusive, exactly like slice(start, end).

Use these pairs when a UI needs to highlight only the captured year, month, or day instead of the entire match.
The same indices as console outputPop 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(".."));
Capture highlighter with the d flag
Highlighting sourcePop 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);}
Captured rangesd

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 full match is 2026-09-26. The highlighted chips use ranges from match.indices, so they line up with the exact characters.

The pattern and sample are length-limited, compiled inside 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.

Practical uses for this lesson
Use caseHow groups help
Parsing datesCapture year, month, and day, then validate or reformat them.
Log linesCapture a timestamp, level, and message while keeping separators out of the result.
Name formattingTurn Last, First into First Last with named replacements or a replacer callback.
Text editor cleanupFind duplicate words with a backreference and replace the repeated copy.
UI highlightingUse 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> turns Last, First into First Last without a manual split.
  • Swapping and cleanup: duplicate-word detection can power editor fixes like replacing the the with the.
  • Highlighting: indices.groups lets 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|dog is 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", \1 must 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.
Similar features that are easy to confuse
QuestionCapturingNon-capturingNamedLookaround preview
Does it consume characters?YesYesYesNo, it checks a position
Does it create a capture slot?YesNoYes, plus a nameNo
Best reason to use itYou need the text laterYou only need groupingYou want readable fieldsYou need context without including it
Lesson scopeThis lessonThis lessonThis lessonNext lesson

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upRead a numbered capture

Predict the exact value logged by the code.

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 group that did not participate

    Type the value printed 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 prints 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 alternation bug

        Write a regex that matches exactly cat or exactly dog, but not longer words.

        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 · ChallengeRequire the same closing quote

          If the pattern had a TODO after .*?, what backreference would require the same quote at the end?

          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

            These questions mix vocabulary with real outputs. For code questions, identify the groups first, then run the final line in your head.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8How are numbered capturing groups assigned?

              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 the named-group destructuring code print on its second line?

              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 value does an optional group produce when it does not participate?

              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$/ when you meant 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 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 d flag 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.

            CompleteFrontend Clear concepts. Working examples.