RegexLab

Capture Groups and Backreferences in Regex

6 min readUpdated September 2026
Programming code on a monitor

Matching text is only half the job; often you need to pull pieces out of it — the year from a date, the domain from an email, the value from a key=value pair. That is what capture groups do. Parentheses around part of a pattern both group it and remember what it matched, so you can read those pieces back or reuse them in a replacement.

Numbered groups

Every pair of parentheses opens a capture group, numbered left to right by its opening bracket. In (\d{4})-(\d{2})-(\d{2}) applied to 2026-08-22, group 1 is 2026, group 2 is 08 and group 3 is 22. The whole match is group 0. In JavaScript the exec method returns an array where index 0 is the full match and the rest are your groups; the tester shows each one labelled $1, $2 and so on beneath the match.

When you only need grouping — say, to apply a quantifier or an alternation — but do not want to capture, use a non-capturing group written (?:...). The pattern (?:https?|ftp):// groups the protocol alternatives without adding a numbered group, which keeps your capture numbers meaningful and the match slightly faster.

Named groups

Counting parentheses gets fragile as patterns grow, so modern regex supports named groups: (?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2}). Now you refer to match.groups.year instead of match[1], and the intent is obvious to anyone reading the code. Named groups are supported in JavaScript (ES2018+) and most current languages, and the tester lists them by name.

Note that the Python-style syntax (?P<name>...) differs from the JavaScript and Java syntax (?<name>...). If you copy a pattern between languages, the group syntax is one of the first things to check.

Backreferences and replacement

A backreference matches the same text a group already captured. Inside the pattern, \1 refers to group 1, so (\w+)\s+\1 finds a doubled word like the the. With named groups you write \k<name>. Backreferences are how you detect repetition that ordinary classes cannot describe.

In the replacement string the same groups are available as $1, $2 or $<name>. To reformat 2026-08-22 into 22/08/2026 you match (\d{4})-(\d{2})-(\d{2}) and replace with $3/$2/$1. The tester's replace box does exactly this live, so you can craft a substitution and copy the result without leaving the page.

Replacement syntax beyond $1

The replacement string understands a small vocabulary of its own. `$1` through `$99` insert numbered groups and `$<name>` inserts a named one; `$&` is the whole match, `` $` `` is everything before it and `$'` everything after; `$$` produces a literal dollar sign. Knowing `$&` alone removes a lot of clumsy patterns — wrapping every match in brackets is `text.replace(/\d+/g, "[$&]")`, with no capture group required.

When the replacement needs logic rather than interpolation, pass a function instead of a string. It receives the whole match first, then each captured group in order, then the offset and the original string — and, if the pattern uses named groups, a final object holding them. That is how you upper-case only the first letter of each word, or convert every matched number to hexadecimal, without leaving the single pass the engine is already making.

What the match object carries

A successful match is an array-like object with extras. Index `0` is the full match and the rest are the numbered groups, `index` is where the match began, `input` is the original string, and `groups` is an object keyed by every named group — `(?<year>\d{4})-(?<month>\d{2})` gives you `m.groups.year` instead of counting parentheses. Naming groups also survives refactoring: inserting a group in the middle renumbers everything after it, but names do not move.

To walk every match with all of that detail, use `matchAll`, which returns an iterator of full match objects and requires the `g` flag. `match` with `g` throws the detail away and hands back plain strings, which is why people fall back to `exec` loops they then get wrong. If you also need the position of each group, add the `d` flag and read `m.indices`, an array of start and end offsets with a `groups` counterpart for named ones.

The takeaway

Parentheses capture the text they match; refer to groups by number, or name them with (?<name>...) for clarity. Use (?:...) when you need grouping without capture, backreferences to match repeats, and $1/$<name> in replacements.

Try it live
Open the regex tester

Try this pattern live with match highlighting.