Regex Groups Flags and Replace in JavaScript Guide

Capture groups, lookahead, lookbehind, named groups, and the replace method turn regex from a search tool into a powerful text transformation engine. Learn every advanced regex feature in JavaScript.

7 min read

JavaScript regex groups, lookaround assertions, and the replace() method are where regex stops being a simple search tool and becomes a text transformation engine. Groups let you pull meaning out of matched text, and lookahead and lookbehind let you match based on context.

replace() with groups lets you reshape strings in one operation. If you are new to regex syntax, start with the regex basics guide before diving into groups.

Capture Groups -- Extracting Parts of a Match

Parentheses create a capture group. The regex engine stores whatever matches inside the parentheses, and you can access it after the match:

javascriptjavascript
const date = /(\d{4})-(\d{2})-(\d{2})/;
const result = date.exec("Today is 2026-07-15");
 
console.log(result[0]); // "2026-07-15" -- full match
console.log(result[1]); // "2026" -- first group
console.log(result[2]); // "07"   -- second group
console.log(result[3]); // "15"   -- third group

The full match is always at index 0. Each capture group appears in order at index 1, 2, and so on.

Groups are especially powerful with replace(). You reference them as $1, $2, and so on in the replacement string:

javascriptjavascript
const usDate = "07/15/2026";
 
// Swap to ISO format: capture month, day, year and reorder
const iso = usDate.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$1-$2");
console.log(iso); // "2026-07-15"

The replacement string reorders the captured pieces using their index. The original string is not modified; replace() returns a new string.

Named Capture Groups

Named groups make capture patterns self-documenting. Instead of remembering that $2 means month, you give the group a name:

javascriptjavascript
const date = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = date.exec("2026-07-15");
 
console.log(result.groups.year);  // "2026"
console.log(result.groups.month); // "07"
console.log(result.groups.day);   // "15"

The syntax wraps a name in angle brackets right after the opening parenthesis. The matched groups appear on result.groups, an object keyed by name. This is far more readable than numeric indices, especially for patterns with four or more groups.

Named groups also work in replace():

javascriptjavascript
const format = "15/07/2026";
const iso = format.replace(
  /(?<day>\d{2})\/(?<month>\d{2})\/(?<year>\d{4})/,
  "$<year>-$<month>-$<day>"
);
console.log(iso); // "2026-07-15"

Use a named placeholder like $<year> instead of a numbered one like $1 in the replacement string when working with named groups.

Non-Capturing Groups

Sometimes you need parentheses to group part of a pattern for a quantifier or alternation, but you do not need to capture the result. Prefix the group with ?: to make it non-capturing:

javascriptjavascript
// Capturing group: stores "http" or "https"
const withCapture = /(https?):\/\/(.+)/;
const captured = withCapture.exec("https://example.com");
console.log(captured[1]); // "https"
 
// Non-capturing group: groups the protocol but does not store it
const withoutCapture = /(?:https?):\/\/(.+)/;
const notCaptured = withoutCapture.exec("https://example.com");
console.log(notCaptured[1]); // "example.com" (the domain, now at index 1)

Non-capturing groups keep your result array clean and are marginally faster since the engine does not need to store the match.

Backreferences -- Matching the Same Text Again

A backreference matches whatever a previous capture group matched, not the same pattern again:

javascriptjavascript
// Match a word that appears twice in a row (like "the the")
const duplicate = /\b(\w+)\s+\1\b/;
 
console.log(duplicate.test("the the word"));    // true
console.log(duplicate.test("the cat word"));    // false -- "the" != "cat"

The backslash-one means "match exactly what group 1 captured," not "match the pattern from group 1 again." This is fundamentally different from repeating the pattern.

Named backreferences use a similar backslash-k syntax with the name in angle brackets:

javascriptjavascript
const quote = /(?<quote>['"])(.+?)\k<quote>/;
 
console.log(quote.test('"hello"'));  // true -- same quote type
console.log(quote.test('"hello\'')); // false -- mismatched quotes

This ensures an opening quote and closing quote are the same character, which a simple character-class pattern without a backreference cannot guarantee.

Lookahead -- Check What Follows

Lookahead asserts that a pattern exists (or does not exist) after the current position, without consuming characters:

SyntaxNameMeaning
?=patternPositive lookaheadMust be followed by pattern
?!patternNegative lookaheadMust NOT be followed by pattern
javascriptjavascript
// Match "JavaScript" only when followed by " tutorial"
console.log(/JavaScript(?= tutorial)/.test("JavaScript tutorial")); // true
console.log(/JavaScript(?= tutorial)/.test("JavaScript basics"));   // false
 
// Match digits not immediately followed by "USD"
const notUSD = /\d+(?!\s*USD)/g;
console.log("100 USD and 200 EUR".match(notUSD)); // ["10", "200"]

The first result is surprising: "10", not "100". The engine tries the full "100" first, sees " USD" right after it, and backtracks one digit at a time until the lookahead succeeds, which happens at "10" because what follows is "0 USD", not "USD".

This is why lookahead with a preceding greedy quantifier needs care: anchor the match with a word boundary or a fixed length when partial matches like this would cause bugs.

A practical use with a cleaner result: match a file extension without including it in the result:

javascriptjavascript
// Match the filename before ".js" without consuming the extension
const jsFiles = /\w+(?=\.js)/g;
console.log("app.js utils.js style.css".match(jsFiles)); // ["app", "utils"]

The lookahead checks that ".js" follows, but the match itself is just the filename. The extension is not consumed, so the next match can start right after the filename.

Lookbehind -- Check What Precedes

Lookbehind does the reverse: it checks what came before the current position. This was added in ES2018 and is supported in all modern browsers:

SyntaxNameMeaning
?<=patternPositive lookbehindMust be preceded by pattern
?<!patternNegative lookbehindMust NOT be preceded by pattern
javascriptjavascript
// Match a number only when preceded by "$"
const prices = "$100 and €200";
console.log(prices.match(/(?<=\$)\d+/g)); // ["100"]
 
// Match a word not preceded by "un"
const notUnPrefixed = /(?<!un)happy/g;
console.log("happy unhappy sad".match(notUnPrefixed)); // ["happy"]
// "happy" at the start matches (not preceded by "un")
// "happy" inside "unhappy" is skipped (preceded by "un")

Lookbehind in JavaScript requires fixed-width patterns. You cannot use open-ended quantifiers inside a lookbehind, but you can use an exact repeat count.

Replace with a Function

The replace() method accepts a callback function instead of a replacement string. The function receives the full match, each capture group, the match offset, and the full input string:

javascriptjavascript
const prices = "item: $10, shipping: $5, tax: $2";
 
const doubled = prices.replace(/\$(\d+)/g, (match, amount) => {
  return `$${Number(amount) * 2}`;
});
 
console.log(doubled); // "item: $20, shipping: $10, tax: $4"

The callback receives "$10" as the full match and "10" as the captured amount. It doubles the number and returns the new string.

This pattern is ideal for computed replacements, conditional logic during substitution, or logging every replacement:

javascriptjavascript
// Convert snake_case to camelCase
function toCamelCase(str) {
  return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
 
console.log(toCamelCase("user_first_name")); // "userFirstName"
console.log(toCamelCase("api_response_time")); // "apiResponseTime"

The underscore before the letter parameter in the callback means "I do not need the full match." It is a convention, not a language feature. The first parameter is always the full match, and subsequent parameters are the capture groups.

Combining Lookaround with Replace

Lookaround and replace together solve problems that neither can solve alone:

javascriptjavascript
// Insert commas as thousands separators: 1000000 → 1,000,000
function addCommas(num) {
  return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
 
console.log(addCommas(1000000));  // "1,000,000"
console.log(addCommas(12345));    // "12,345"

This pattern is dense but worth understanding. It matches a position that is not a word boundary, is followed by groups of three digits, and is not followed by another digit. At each such position, it inserts a comma.

A Practical Example -- URL Slug Generator

Here is a function that combines groups, replace, and flags to convert a title into a URL-friendly slug:

javascriptjavascript
function slugify(title) {
  return title
    .toLowerCase()
    .replace(/[^\w\s-]/g, "")  // Remove special characters
    .replace(/\s+/g, "-")      // Replace whitespace with hyphens
    .replace(/-+/g, "-")       // Collapse multiple hyphens
    .replace(/^-|-$/g, "");    // Trim leading/trailing hyphens
}
 
console.log(slugify("Hello, World! This Is JavaScript")); // "hello-world-this-is-javascript"
console.log(slugify("What??? Is -- Regex???"));            // "what-is-regex"

Each replace() call handles one transformation. Chaining them makes the logic easy to read and modify. This is cleaner than one monster regex that tries to do everything at once.

The Full Flag Reference

All six flags, with the contexts where each matters:

FlagUse when
gYou need all matches, not just the first. Required for iterating with exec() or getting multiple results from match().
iCase does not matter. Use for user-facing search, not for parsing identifiers or keywords.
mYou are matching against multiline text and the start/end anchors should match per-line positions.
sYou need the dot to match newlines. Useful for matching across lines in templates or formatted text.
uYou work with Unicode characters beyond ASCII, use property escapes, or need astral plane characters.
yYou need to match exactly at lastIndex and nowhere else, typically in tokenizers or incremental parsers.

For foundational regex syntax (literals, character classes, quantifiers, anchors, test() / exec()), see /javascript/javascript-regular-expressions-complete-guide. For string methods that accept regex, see /javascript/javascript-string-methods-complete-guide.

Rune AI

Rune AI

Key Insights

  • Capture groups (...) extract parts of a match and make them available for backreferences and replacement.
  • Named groups (?<name>...) make capture groups self-documenting and accessible by name in the result.
  • Lookahead (?=...) and lookbehind (?<=...) match based on surrounding context without consuming characters.
  • replace() supports $1, $2 backreferences and a callback function for dynamic replacements.
  • Non-capturing groups (?:...) group parts of a pattern without storing the matched text.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between a capturing and non-capturing group?

A capturing group (parentheses) stores the matched text and makes it available in the result array, backreferences, and replace() patterns. A non-capturing group (?:...) groups part of the pattern without storing the match, which is slightly faster and keeps the result array cleaner.

When should I use lookahead vs lookbehind?

Use lookahead (?=...) or (?!...) when you want to check what comes after the current position without including it in the match. Use lookbehind (?&lt;=...) or (?&lt;!...) when you want to check what comes before. Lookbehind was added in ES2018 and is supported in all modern browsers.

Conclusion

Capture groups, lookaround assertions, and the replace method turn regex from a search tool into a text transformation engine. Groups let you extract meaning from matched text. Lookahead and lookbehind let you match based on context without consuming it. And replace() with groups and functions lets you reshape strings however you need. These features together cover most real-world regex tasks beyond simple validation.