JAVASCRIPT / REGULAR EXPRESSIONS
Groups, alternation, and backreferences
Group parts of a pattern with () and (?:), control exactly what | applies to, and reuse captured text with \1 in patterns and $1 in replacements.
What you will learn
- Fence a choice with (?:a|b) so | applies to that part, not the whole pattern
- Number groups by their opening parentheses, left to right, skipping (?:...)
- Reuse captured text with \1 inside the pattern and $1 inside a replacement
- Expect undefined in m[n] for a branch that never ran, but empty text from $n
Understanding Groups, alternation, and backreferences
A pair of parentheses does two separate jobs. It bounds a piece of the pattern so an operator applies to that whole piece — (?:ab)+ repeats ab, while ab+ only repeats the b — and, unless you write (?:, it also records whatever text that piece matched into a numbered slot. The numbers come from the source text, not from the match: count opening parentheses left to right, so the first one is group 1 no matter how deeply groups nest or whether that branch ever runs. Reach for a non-capturing group whenever you only need bundling, because every capturing group you leave behind is a slot you have to track and renumber when you edit the pattern.
The | operator has the lowest precedence of anything in a pattern, so it cuts the enclosing group into branches — or the entire regex, if there is no enclosing group. Read /get|set value/ as "either get, or set value", which is almost never the intent; parentheses are how you say "this choice, right here". Branches are also ordered rather than measured: at each starting position the engine tries them left to right and keeps the first that succeeds, so /Jan|January/ against "January" returns just "Jan". Put the more specific alternative first, or hoist the shared text out of the choice, as in (?:on|off)line.
A backreference demands that the same text appear again. During a match attempt group 1 holds a concrete substring, and \1 matches exactly that substring, which is why /([ab])\1/ accepts "aa" and "bb" but rejects "ab", and why /\b(\w+) \1\b/ can catch accidental word doubling that no fixed pattern could describe. The captured value is per attempt: as the engine backtracks or slides to the next starting index the group is refilled, so \1 means something different each time. Inside a pattern the syntax is \1; in the replacement string of replace the same slot is $1 — same slots, different syntax, and the two are not interchangeable.
const semver = /v(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta))?/;
const m = "v2.14.7-beta".match(semver);
console.log(m[0], "->", m[1], m[2], m[3], m[4]);
const plain = "v3.0.1".match(semver);
console.log(plain[0], "->", plain[4]);
const doubled = /\b(\w+)\s+\1\b/;
console.log(doubled.test("it is is broken"));
console.log("it is is broken".replace(doubled, "$1"));Parentheses simultaneously bound what an operator applies to and record the text they matched, and a backreference replays that recorded text rather than the pattern that captured it.
Worked examples
Alternation reaches further than you think
Shows that | splits the whole pattern and that branches are tried in order, not by length.
const loose = /Mr|Mrs Jones/;
const tight = /(?:Mr|Mrs) Jones/;
console.log("Mrs Jones".match(loose)[0]);
console.log("Mrs Jones".match(tight)[0]);
console.log(loose.test("Mr Bean"), tight.test("Mr Bean"));Example explained
Line 1/Mr|Mrs Jones/ splits at the top level into Mr and Mrs Jones, so the space and Jones belong to the second branch only.
Line 2At index 0 the engine tries Mr first, it succeeds, and the match ends there — the longer branch is never attempted.
Line 3In tight, the choice is fenced, so " Jones" is required after either title: Mr matches, the space fails, and the engine backtracks into Mrs.
Line 4That fencing is also why tight rejects "Mr Bean" while loose accepts it on the bare Mr branch.
Rewriting with $n and repeating with \1
Reorders captures in a replacement and uses a backreference to collapse a repeated character.
const dates = "due 2026-09-03, ship 2026-12-25";
console.log(dates.replace(/(\d{4})-(\d{2})-(\d{2})/g, "$3/$2/$1"));
console.log("Wow!!!! Really???".replace(/([!?])\1+/g, "$1"));
console.log(/([!?])\1/.test("!?"));Example explained
Line 1$3/$2/$1 reorders the three captures; the slashes and slot references live in the replacement string, never in the pattern.
Line 2([!?])\1+ requires two or more copies of whichever mark group 1 captured, so !!!! collapses to ! and ??? to ?.
Line 3"!?" fails because \1 demands the character ! again rather than any member of [!?] — a backreference replays text, not the class.
How slots get their numbers
Demonstrates numbering across nested groups and shows that a non-capturing group claims no slot.
const m = "size 1280x720 px".match(/((\d+)x(\d+))(?: px)?/);
console.log(m[0]);
console.log(m[1]);
console.log(m[2], m[3]);
console.log(m.length, m.index);Example explained
Line 1Slots follow the order of the opening parentheses, so the outer group is 1 and the nested width and height are 2 and 3.
Line 2(?: px)? still contributes text to m[0], but it claims no slot, which is why m.length is 4 and not 5.
Line 3m[0] is always the whole match, and m.index reports where it began — 5 here, because "size " was skipped.
Important notes
A backreference to a group that never participated matches the empty string in JavaScript instead of failing, so /(?:(a)|b)\1/.test("b") is true; that same group reads as undefined in the result array, while $1 in a replacement inserts nothing.
match with the g flag returns only whole matches and discards every group slot, so use exec, matchAll, or a non-global regex when you need m[1].
Common mistakes
Writing /copy|move file/ and assuming the word file is required: | splits the whole pattern, so the bare text copy anywhere in the input passes and the check waves through data it should reject. The fix is /(?:copy|move) file/.
Assuming \1 re-runs the group's pattern: /([ab])\1/ matches "aa" and "bb" but not "ab", so a "two of these characters" test silently misses the mixed cases you expected it to catch.
Mixing the two syntaxes: \1 works only inside a pattern and $1 only inside a replacement string, so "aa".replace(/(a)\1/, "\1") inserts control character U+0001 instead of "a" — and is a SyntaxError in a module, where octal escapes in strings are banned.
Try it yourself
Change, predict, then run
In a browser console, write one regex with replace that turns both "Curie, Marie" and "Curie;Marie" into "Marie Curie", using two capture groups and a fenced (?:,|;) separator. Then write a second regex that uses a backreference to report which word is doubled in "she she left early".
Open the JavaScript workspaceCheck your understanding
After const m = "b".match(/(?:(a)|(b))x?/), what do m[1] and m[2] hold, and why?
- m[1] is undefined and m[2] is "b", because only the branch that actually ran recorded any text
- m[1] is "" and m[2] is "b", because a group that is skipped captures the empty string
- m[1] is "b" and m[2] is undefined, because numbering follows whichever branch matched
- Both are undefined, because (?:...) suppresses capturing for the groups nested inside it
Show answer
Slot numbers are fixed by the position of the opening parentheses in the pattern source, so (a) is permanently group 1 even though the engine abandoned that branch; a group that never participates is undefined in the array. Option 2 is tempting because $1 in a replacement string inserts "" for such a group, but that substitution rule does not change the array value. And (?:...) only stops itself from capturing — the (a) and (b) inside it still get slots 1 and 2.