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.
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:
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 groupThe 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:
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:
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():
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:
// 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:
// 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:
const quote = /(?<quote>['"])(.+?)\k<quote>/;
console.log(quote.test('"hello"')); // true -- same quote type
console.log(quote.test('"hello\'')); // false -- mismatched quotesThis 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:
| Syntax | Name | Meaning |
|---|---|---|
| ?=pattern | Positive lookahead | Must be followed by pattern |
| ?!pattern | Negative lookahead | Must NOT be followed by pattern |
// 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:
// 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:
| Syntax | Name | Meaning |
|---|---|---|
| ?<=pattern | Positive lookbehind | Must be preceded by pattern |
| ?<!pattern | Negative lookbehind | Must NOT be preceded by pattern |
// 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:
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:
// 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:
// 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:
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:
| Flag | Use when |
|---|---|
| g | You need all matches, not just the first. Required for iterating with exec() or getting multiple results from match(). |
| i | Case does not matter. Use for user-facing search, not for parsing identifiers or keywords. |
| m | You are matching against multiline text and the start/end anchors should match per-line positions. |
| s | You need the dot to match newlines. Useful for matching across lines in templates or formatted text. |
| u | You work with Unicode characters beyond ASCII, use property escapes, or need astral plane characters. |
| y | You 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
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.
Frequently Asked Questions
What is the difference between a capturing and non-capturing group?
When should I use lookahead vs lookbehind?
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.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.