How to Use Capturing Groups in JavaScript Regular Expressions
Regular expressions let you match patterns, but often matching alone is not enough. You need to extract specific parts of the match. This is exactly what capturing groups do. By wrapping a portion of your pattern in parentheses ( ), you tell the regex engine to remember that part separately, making it available for extraction, backreferencing, and replacement.
Capturing groups are one of the most frequently used regex features in real-world JavaScript. They power everything from parsing URLs and dates to reformatting strings and validating structured input. This guide covers basic grouping, all the methods for accessing captured values, named groups for readability, non-capturing groups for performance, and the powerful use of captures inside replace().
Grouping with Parentheses ( )
Parentheses in a regular expression serve two purposes simultaneously:
- Grouping: They treat multiple characters as a single unit for quantifiers and alternation
- Capturing: They save the matched content so you can access it later
Basic Grouping
Without parentheses, a quantifier applies only to the single element immediately before it:
// Without grouping: + applies only to 'a'
const noGroup = /ha+/;
console.log("haaaa".match(noGroup)[0]); // 'haaaa' ('h' then one or more 'a')
console.log("haha".match(noGroup)[0]); // 'ha' (just 'h' and one 'a')
// With grouping: + applies to the entire 'ha' sequence
const withGroup = /(ha)+/;
console.log("hahaha".match(withGroup)[0]); // 'hahaha' ('ha' repeated 3 times)
console.log("haaaa".match(withGroup)[0]); // 'ha' (only one 'ha' occurrence)
How Capturing Works
When a pattern matches, the regex engine stores the overall match plus each group's content separately. Groups are numbered from left to right, starting at 1:
const pattern = /(\d{4})-(\d{2})-(\d{2})/;
const str = "Date: 2024-01-15";
const result = str.match(pattern);
console.log(result[0]); // '2024-01-15' (the full match)
console.log(result[1]); // '2024' (group 1 (year))
console.log(result[2]); // '01' (group 2 (month))
console.log(result[3]); // '15' (group 3 (day))
The full match is always at index 0. Group 1 is at index 1, group 2 at index 2, and so on.
Nested Groups
When groups are nested, they are numbered by the position of their opening parenthesis, reading left to right:
const pattern = /(((\d{2})\/(\d{2}))\/(\d{4}))/;
const str = "15/01/2024";
const result = str.match(pattern);
console.log(result[0]); // '15/01/2024' (full match)
console.log(result[1]); // '15/01/2024' (group 1: outermost ( ))
console.log(result[2]); // '15/01' (group 2: ((\d{2})\/(\d{2})))
console.log(result[3]); // '15' (group 3: first (\d{2}))
console.log(result[4]); // '01' (group 4: second (\d{2}))
console.log(result[5]); // '2024' (group 5: (\d{4}))
To determine the number of any group, count the opening parentheses ( from left to right:
( ( ( \d{2} ) \/ ( \d{2} ) ) \/ ( \d{4} ) )
↑ ↑ ↑ ↑ ↑
1 2 3 4 5
Groups with Alternation
Parentheses are essential for controlling the scope of the alternation operator |:
// Without grouping: matches "cat" OR "dog"
const noGroup = /cat|dog/;
// With grouping: matches "I have a cat" OR "I have a dog"
const withGroup = /I have a (cat|dog)/;
console.log(withGroup.test("I have a cat")); // true
console.log(withGroup.test("I have a dog")); // true
console.log(withGroup.test("I have a bird")); // false
const result = "I have a cat".match(withGroup);
console.log(result[0]); // 'I have a cat'
console.log(result[1]); // 'cat' (the captured alternative)
Optional Groups
A group can be made optional with ?. If the group does not participate in the match, its captured value is undefined:
const pattern = /(\d{4})-(\d{2})(-(\d{2}))?/;
// Full date
const full = "2024-01-15".match(pattern);
console.log(full[1]); // '2024'
console.log(full[2]); // '01'
console.log(full[3]); // '-15' (the optional group (including hyphen))
console.log(full[4]); // '15' (the inner group (day only))
// Partial date (no day)
const partial = "2024-01".match(pattern);
console.log(partial[1]); // '2024'
console.log(partial[2]); // '01'
console.log(partial[3]); // undefined (optional group did not match)
console.log(partial[4]); // undefined (inner group did not match either)
Accessing Captures: match(), matchAll(), exec()
JavaScript provides three primary ways to access captured groups. Each has different behavior and use cases.
str.match(regex) Without the g Flag
When match() is called without the g flag, it returns a result array containing the full match and all captured groups, plus two extra properties: index (the position of the match) and input (the original string).
const pattern = /(\w+)@(\w+)\.(\w+)/;
const str = "Contact: user@example.com for info";
const result = str.match(pattern);
console.log(result[0]); // 'user@example.com' (full match)
console.log(result[1]); // 'user' (group 1)
console.log(result[2]); // 'example' (group 2)
console.log(result[3]); // 'com' (group 3)
console.log(result.index); // 9 (match position)
console.log(result.input); // 'Contact: user@example.com for info'
str.match(regex) With the g Flag
When the g flag is present, match() returns an array of all full matches but discards all capturing group information:
const pattern = /(\w+)@(\w+)\.(\w+)/g;
const str = "Email user@example.com or admin@test.org";
const result = str.match(pattern);
console.log(result);
// Output: ['user@example.com', 'admin@test.org']
// No group information! Only full matches.
This is one of the most common sources of confusion. If you need both the g flag (multiple matches) and group captures, use matchAll() or exec() instead.
str.matchAll(regex): The Modern Solution
matchAll() returns an iterator of all matches, and each match includes the full group information. The regex must have the g flag.
const pattern = /(\w+)@(\w+)\.(\w+)/g;
const str = "Email user@example.com or admin@test.org";
const matches = str.matchAll(pattern);
for (const match of matches) {
console.log(`Full: ${match[0]}`);
console.log(`User: ${match[1]}`);
console.log(`Domain: ${match[2]}`);
console.log(`TLD: ${match[3]}`);
console.log(`Index: ${match.index}`);
console.log("---");
}
// Output:
// Full: user@example.com
// User: user
// Domain: example
// TLD: com
// Index: 6
// ---
// Full: admin@test.org
// User: admin
// Domain: test
// TLD: org
// Index: 26
// ---
You can also convert the iterator to an array with the spread operator:
const pattern = /(\d{4})-(\d{2})-(\d{2})/g;
const str = "Dates: 2024-01-15 and 2023-12-25";
const allMatches = [...str.matchAll(pattern)];
console.log(allMatches.length); // 2
console.log(allMatches[0][0]); // '2024-01-15'
console.log(allMatches[0][1]); // '2024'
console.log(allMatches[1][0]); // '2023-12-25'
console.log(allMatches[1][3]); // '25'
regex.exec(str): Full Control
The exec() method is called on the regex object itself. It returns one match at a time (with full group information) and updates the regex's lastIndex property so subsequent calls find the next match.
const pattern = /(\w+)=(\w+)/g;
const str = "name=John age=30 city=NYC";
let match;
while ((match = pattern.exec(str)) !== null) {
console.log(`Key: ${match[1]}, Value: ${match[2]} at index ${match.index}`);
}
// Output:
// Key: name, Value: John at index 0
// Key: age, Value: 30 at index 10
// Key: city, Value: NYC at index 17
| Method | Multiple Matches | Group Access | Best For |
|---|---|---|---|
match() without g | No (first only) | Yes | Single match extraction |
match() with g | Yes | No | Listing all full matches |
matchAll() | Yes | Yes | Modern code, iterating all matches with groups |
exec() | Yes (manual loop) | Yes | Fine-grained control, conditional logic per match |
For most use cases in modern JavaScript, matchAll() is the best choice.
Comparison Example
const pattern = /(\d+)([a-z]+)/g;
const str = "12abc 34def 56ghi";
// match() with g: only full matches, no groups
console.log(str.match(pattern));
// Output: ['12abc', '34def', '56ghi']
// matchAll(): full matches WITH groups
for (const m of str.matchAll(pattern)) {
console.log(`${m[0]} → digits: ${m[1]}, letters: ${m[2]}`);
}
// Output:
// 12abc → digits: 12, letters: abc
// 34def → digits: 34, letters: def
// 56ghi → digits: 56, letters: ghi
// exec(): same result, manual loop
const regex = /(\d+)([a-z]+)/g;
let m;
while ((m = regex.exec(str)) !== null) {
console.log(`${m[0]} → digits: ${m[1]}, letters: ${m[2]}`);
}
// Same output as matchAll
Named Groups: (?<name>...)
As patterns grow more complex, referring to groups by number (result[1], result[2]) becomes fragile and hard to read. Named groups let you assign meaningful labels.
Syntax
Place ?<name> immediately after the opening parenthesis of a group:
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const str = "2024-01-15";
const result = str.match(pattern);
// Access by number (still works)
console.log(result[1]); // '2024'
console.log(result[2]); // '01'
console.log(result[3]); // '15'
// Access by name via the .groups property
console.log(result.groups.year); // '2024'
console.log(result.groups.month); // '01'
console.log(result.groups.day); // '15'
The groups property is an object containing all named captures. This makes code self-documenting and resistant to breaking when you add or reorder groups.
Named Groups with matchAll()
const pattern = /(?<key>\w+)=(?<value>[^&]+)/g;
const queryString = "name=John&age=30&city=NYC";
for (const match of queryString.matchAll(pattern)) {
console.log(`${match.groups.key}: ${match.groups.value}`);
}
// Output:
// name: John
// age: 30
// city: NYC
Named Groups with Destructuring
Named groups pair beautifully with JavaScript's destructuring syntax:
const pattern = /(?<hours>\d{2}):(?<minutes>\d{2}):(?<seconds>\d{2})/;
const str = "Current time: 14:30:45";
const { groups: { hours, minutes, seconds } } = str.match(pattern);
console.log(hours); // '14'
console.log(minutes); // '30'
console.log(seconds); // '45'
// Destructuring inside a loop
const pattern = /(?<name>\w+)\s*:\s*(?<score>\d+)/g;
const leaderboard = "Alice: 95, Bob: 87, Charlie: 92";
for (const { groups: { name, score } } of leaderboard.matchAll(pattern)) {
console.log(`${name} scored ${score}`);
}
// Output:
// Alice scored 95
// Bob scored 87
// Charlie scored 92
Named Groups and exec()
const pattern = /(?<protocol>https?):\/\/(?<domain>[^/]+)(?<path>\/.*)?/;
const url = "https://example.com/products/123";
const result = pattern.exec(url);
console.log(result.groups.protocol); // 'https'
console.log(result.groups.domain); // 'example.com'
console.log(result.groups.path); // '/products/123'
Mixing Named and Unnamed Groups
You can mix named and unnamed groups in the same pattern. Unnamed groups still receive numeric indices, and the numbered indices include both named and unnamed groups:
const pattern = /(?<year>\d{4})-(\d{2})-(?<day>\d{2})/;
const str = "2024-01-15";
const result = str.match(pattern);
console.log(result[1]); // '2024' (group 1 (named: year))
console.log(result[2]); // '01' (group 2 (unnamed))
console.log(result[3]); // '15' (group 3 (named: day))
console.log(result.groups.year); // '2024'
console.log(result.groups.day); // '15'
// result.groups has no entry for the unnamed group
When a pattern has more than two or three groups, always use named groups. The readability improvement is significant, and destructuring makes the code elegant and maintainable.
// ❌ Hard to remember what [1], [2], [3], [4] mean
const match = str.match(/(\w+)\s+(\d+)\s+(\w+)\s+(\d+\.\d+)/);
const name = match[1];
const qty = match[2];
// ✅ Self-documenting
const match2 = str.match(/(?<product>\w+)\s+(?<qty>\d+)\s+(?<unit>\w+)\s+(?<price>\d+\.\d+)/);
const { product, qty: quantity, unit, price } = match2.groups;
Non-Capturing Groups: (?: ...)
Sometimes you need parentheses for grouping (to apply a quantifier or control alternation) but you do not care about capturing the matched content. Non-capturing groups solve this. They group without storing the result.
Syntax
Place ?: immediately after the opening parenthesis:
// Capturing group: stores the match
const capturing = /(abc)+/;
// Non-capturing group: groups but does NOT store
const nonCapturing = /(?:abc)+/;
Why Non-Capturing Groups Matter
1. They keep your group numbering clean:
// ❌ With capturing: the alternation group shifts other group numbers
const pattern = /(Mr|Mrs|Ms)\.\s(\w+)\s(\w+)/;
const str = "Mrs. Jane Smith";
const result = str.match(pattern);
console.log(result[1]); // 'Mrs' (group 1 (title))
console.log(result[2]); // 'Jane' (group 2 (first name))
console.log(result[3]); // 'Smith' (group 3 (last name))
// ✅ With non-capturing: the title is grouped but not numbered
const patternNC = /(?:Mr|Mrs|Ms)\.\s(\w+)\s(\w+)/;
const resultNC = str.match(patternNC);
console.log(resultNC[1]); // 'Jane' (group 1 (first name))
console.log(resultNC[2]); // 'Smith' (group 2 (last name))
// The title alternation is not captured (cleaner numbering)
2. They slightly improve performance:
Capturing has a small overhead because the engine must store the matched substring. In patterns used repeatedly (especially in loops or on large inputs), non-capturing groups avoid this overhead.
3. They signal intent:
Non-capturing groups tell other developers (and your future self) that the grouping is structural, not for extraction.
Non-Capturing Groups with Quantifiers
// Match a file extension that may repeat (like .tar.gz)
const pattern = /\w+(?:\.\w+)+/g;
const str = "archive.tar.gz image.png backup.tar.bz2";
console.log(str.match(pattern));
// Output: ['archive.tar.gz', 'image.png', 'backup.tar.bz2']
// The (?:\.\w+) group is used for the + quantifier but not captured
// Match an IP address without capturing each octet
const ip = /(?:\d{1,3}\.){3}\d{1,3}/g;
const log = "Server 192.168.1.1 received request from 10.0.0.5";
console.log(log.match(ip));
// Output: ['192.168.1.1', '10.0.0.5']
Combining Capturing and Non-Capturing Groups
A common pattern uses non-capturing groups for structure and capturing groups only for the parts you need:
// Extract the protocol and domain, but not the optional port group
const urlPattern = /(?<protocol>https?):\/\/(?<domain>[^:/]+)(?::\d+)?(?<path>\/[^\s]*)?/;
const url = "https://example.com:8080/api/users";
const { groups } = url.match(urlPattern);
console.log(groups.protocol); // 'https'
console.log(groups.domain); // 'example.com'
console.log(groups.path); // '/api/users'
// The port (:8080) matched via (?::\d+)? but was not captured
// Parse a CSS color function
// rgb(255, 128, 0) or rgba(255, 128, 0, 0.5)
const colorPattern = /(?:rgba?)\((?<r>\d+),\s*(?<g>\d+),\s*(?<b>\d+)(?:,\s*(?<a>[\d.]+))?\)/;
const color1 = "rgb(255, 128, 0)".match(colorPattern);
console.log(color1.groups);
// Output: { r: '255', g: '128', b: '0', a: undefined }
const color2 = "rgba(255, 128, 0, 0.5)".match(colorPattern);
console.log(color2.groups);
// Output: { r: '255', g: '128', b: '0', a: '0.5' }
Side-by-Side: Capturing vs. Non-Capturing
const str = "2024-01-15";
// All groups capturing
const allCapture = /(\d{4})-(\d{2})-(\d{2})/;
const r1 = str.match(allCapture);
console.log(r1.length - 1); // 3 groups captured
// Only year and day capturing, month non-capturing
const selective = /(\d{4})-(?:\d{2})-(\d{2})/;
const r2 = str.match(selective);
console.log(r2.length - 1); // 2 groups captured
console.log(r2[1]); // '2024' (year)
console.log(r2[2]); // '15' (day, now group 2 instead of 3)
Capturing Groups in replace(): $1, $2, $<name>
One of the most powerful applications of capturing groups is in string replacement. The replace() and replaceAll() methods allow you to reference captured groups in the replacement string.
Numbered References: $1, $2, etc.
In the replacement string, $1 refers to the first capturing group, $2 to the second, and so on:
// Reformat a date from YYYY-MM-DD to DD/MM/YYYY
const pattern = /(\d{4})-(\d{2})-(\d{2})/g;
const str = "Dates: 2024-01-15 and 2023-12-25";
const reformatted = str.replace(pattern, "$3/$2/$1");
console.log(reformatted);
// Output: 'Dates: 15/01/2024 and 25/12/2023'
// Swap first and last name
const swapNames = /(\w+)\s(\w+)/;
const name = "John Smith";
console.log(name.replace(swapNames, "$2, $1"));
// Output: 'Smith, John'
// Wrap words in HTML tags
const words = /(\w+)/g;
const str = "Hello World";
console.log(str.replace(words, "<b>$1</b>"));
// Output: '<b>Hello</b> <b>World</b>'
Special Replacement Patterns
The replacement string supports several special $ patterns:
| Pattern | Meaning |
|---|---|
$1, $2, ... $99 | Content of the nth capturing group |
$& | The entire matched substring |
$` | The portion of the string before the match |
$' | The portion of the string after the match |
$$ | A literal $ character |
$<name> | Content of the named group |
// $& - reference the full match
const str = "hello world";
console.log(str.replace(/\w+/g, "[$&]"));
// Output: '[hello] [world]'
// $$ - insert a literal dollar sign
const price = "100";
console.log(price.replace(/(\d+)/, "$$$1.00"));
// Output: '$100.00'
// $` and $' - before and after the match
const str = "abc-def-ghi";
const result = str.replace(/-def-/, '($`|$&|$\')');
// This can be confusing, so use sparingly
Named References: $<name>
When using named capturing groups, you can reference them by name in the replacement string:
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;
const str = "Date: 2024-01-15";
const result = str.replace(pattern, "$<day>/$<month>/$<year>");
console.log(result);
// Output: 'Date: 15/01/2024'
// Convert "lastName, firstName" to "firstName lastName"
const pattern = /(?<last>\w+),\s*(?<first>\w+)/g;
const list = "Smith, John\nDoe, Jane\nBrown, Bob";
const result = list.replace(pattern, "$<first> $<last>");
console.log(result);
// Output:
// John Smith
// Jane Doe
// Bob Brown
Replacement with a Function
For complex replacements, you can pass a function instead of a string. The function receives the full match and all groups as arguments:
// Function signature: (fullMatch, group1, group2, ..., index, originalString, namedGroups)
const pattern = /(?<word>\w+)/g;
const str = "hello world";
const result = str.replace(pattern, (fullMatch, word, index, original, groups) => {
return word.charAt(0).toUpperCase() + word.slice(1);
});
console.log(result);
// Output: 'Hello World'
// Convert pixel values to rem
const pattern = /(\d+)px/g;
const css = "font-size: 16px; margin: 24px; padding: 8px;";
const result = css.replace(pattern, (match, pixels) => {
return (parseInt(pixels) / 16) + "rem";
});
console.log(result);
// Output: 'font-size: 1rem; margin: 1.5rem; padding: 0.5rem;'
// Censor email addresses, keeping only the domain
const pattern = /(?<user>[\w.]+)@(?<domain>[\w.]+)/g;
const text = "Contact alice@example.com or bob@company.org";
const censored = text.replace(pattern, (match, user, domain, index, str, groups) => {
return `***@${groups.domain}`;
});
console.log(censored);
// Output: 'Contact ***@example.com or ***@company.org'
When a function is used, named groups are passed as the last argument (an object), following the index and originalString arguments:
const pattern = /(?<a>\d+)\+(?<b>\d+)/g;
const str = "Calculate: 3+5 and 10+20";
const result = str.replace(pattern, (...args) => {
// Named groups are the last argument
const groups = args[args.length - 1];
const sum = parseInt(groups.a) + parseInt(groups.b);
return `${groups.a}+${groups.b}=${sum}`;
});
console.log(result);
// Output: 'Calculate: 3+5=8 and 10+20=30'
Using replaceAll() with Groups
replaceAll() works identically to replace() with the g flag, but requires the g flag to be present on the regex:
const pattern = /(?<first>\w)(\w*)/g;
const str = "hello world foo bar";
const result = str.replaceAll(pattern, (match, first, rest, idx, original, groups) => {
return groups.first.toUpperCase() + rest;
});
console.log(result);
// Output: 'Hello World Foo Bar'
Real-World Examples
Parsing a Log File Line
const logPattern = /^(?<timestamp>\d{4}-\d{2}-\d{2}T[\d:.]+)\s+\[(?<level>\w+)\]\s+(?<message>.+)$/;
const line = "2024-01-15T14:30:45.123 [ERROR] Database connection failed";
const { groups } = line.match(logPattern);
console.log(groups.timestamp); // '2024-01-15T14:30:45.123'
console.log(groups.level); // 'ERROR'
console.log(groups.message); // 'Database connection failed'
Converting Markdown Links to HTML
const mdLinkPattern = /\[(?<text>[^\]]+)\]\((?<url>[^)]+)\)/g;
const markdown = "Visit [Google](https://google.com) or [GitHub](https://github.com)";
const html = markdown.replace(mdLinkPattern, '<a href="$<url>">$<text></a>');
console.log(html);
// Output: 'Visit <a href="https://google.com">Google</a> or <a href="https://github.com">GitHub</a>'
Extracting Function Calls from Code
const funcCallPattern = /(?<name>\w+)\((?<args>[^)]*)\)/g;
const code = "let result = add(1, 2) + multiply(3, 4);";
for (const { groups } of code.matchAll(funcCallPattern)) {
console.log(`Function: ${groups.name}, Arguments: ${groups.args}`);
}
// Output:
// Function: add, Arguments: 1, 2
// Function: multiply, Arguments: 3, 4
Reformatting Phone Numbers
const phonePattern = /(?<area>\d{3})[-.]?(?<prefix>\d{3})[-.]?(?<line>\d{4})/g;
const text = "Call 5551234567 or 555-123-4567 or 555.123.4567";
const formatted = text.replace(phonePattern, "($<area>) $<prefix>-$<line>");
console.log(formatted);
// Output: 'Call (555) 123-4567 or (555) 123-4567 or (555) 123-4567'
Building a Simple Template Engine
function renderTemplate(template, data) {
return template.replace(/\{\{(?<key>\w+)\}\}/g, (match, key) => {
return key in data ? data[key] : match;
});
}
const template = "Hello {{name}}, welcome to {{city}}! Your code is {{code}}.";
const data = { name: "Alice", city: "Berlin" };
console.log(renderTemplate(template, data));
// Output: 'Hello Alice, welcome to Berlin! Your code is {{code}}.'
// {{code}} was not in data, so it stays unchanged
Common Mistakes
Mistake 1: Using match() with g and Expecting Groups
const pattern = /(\w+)=(\w+)/g;
const str = "a=1 b=2 c=3";
// ❌ match() with g flag drops group information
const wrong = str.match(pattern);
console.log(wrong);
// Output: ['a=1', 'b=2', 'c=3'] - no groups!
// ✅ Use matchAll() instead
const correct = [...str.matchAll(pattern)];
console.log(correct[0][1]); // 'a'
console.log(correct[0][2]); // '1'
console.log(correct[1][1]); // 'b'
console.log(correct[1][2]); // '2'
Mistake 2: Group Numbering Confusion with Non-Capturing Groups
const pattern = /(?:https?:\/\/)?(\w+)\.(\w+)/;
const str = "https://example.com";
const result = str.match(pattern);
// ❌ Wrong: expecting group 1 to be "https"
// The (?:...) is non-capturing, so it has no number
// ✅ Correct numbering
console.log(result[1]); // 'example' (first CAPTURING group)
console.log(result[2]); // 'com' (second CAPTURING group)
Mistake 3: Forgetting g Flag with matchAll()
const pattern = /(\d+)/;
const str = "1 2 3";
// ❌ TypeError: matchAll requires the g flag
try {
str.matchAll(pattern);
} catch (e) {
console.log(e.message);
// "String.prototype.matchAll called with a non-global RegExp argument"
}
// ✅ Add the g flag
const correct = [...str.matchAll(/(\d+)/g)];
console.log(correct.length); // 3
Mistake 4: Using Capturing Groups When You Only Need Grouping
// ❌ Unnecessary capturing: creates unused groups
const bad = /(https?):\/\/(www\.)?(\w+)\.(\w+)/;
// 4 capturing groups when you might only need the domain
// ✅ Use non-capturing for structural groups
const good = /(?:https?):\/\/(?:www\.)?(?<domain>\w+)\.(?<tld>\w+)/;
const result = "https://www.example.com".match(good);
console.log(result.groups.domain); // 'example'
console.log(result.groups.tld); // 'com'
// Clean: only the groups you actually need are captured
Mistake 5: $ References in Template Literal Strings
When using template literals with replace(), be careful that $ signs are not interpreted by JavaScript's template literal engine:
const pattern = /(\w+)/g;
const str = "hello";
// ❌ Potential confusion: template literals process ${} as JavaScript expressions
// But $1, $2 are NOT JavaScript expressions: they are regex replacement tokens
// They only work inside the replacement string of .replace()
// This works fine because $1 is not a valid JS template expression:
const result = str.replace(pattern, `<$1>`);
console.log(result); // '<hello>'
// But this would be a problem:
// str.replace(pattern, `${someVar}$1`)
// The ${someVar} is JS, the $1 is regex: mixing can be confusing
// For clarity, use a regular string: str.replace(pattern, someVar + "$1")
Quick Reference
| Feature | Syntax | Purpose |
|---|---|---|
| Capturing group | (pattern) | Group and capture |
| Named capturing group | (?<name>pattern) | Group, capture, and name |
| Non-capturing group | (?:pattern) | Group only, no capture |
Access group in match() | result[1], result[2] | By index |
| Access named group | result.groups.name | By name |
| Replace with group | $1, $2 | Numbered reference |
| Replace with named group | $<name> | Named reference |
| Replace with full match | $& | Entire match |
| Replace with function | (match, g1, g2, ...) => {} | Dynamic replacement |
Capturing groups transform regular expressions from simple pattern matchers into powerful data extraction and transformation tools. Use numbered groups for simple patterns, named groups for readability in complex patterns, and non-capturing groups whenever you need structure without extraction. Combined with matchAll() and function-based replace(), capturing groups handle the majority of text processing tasks you will encounter in JavaScript development.