Atomic groups in JavaScript: emulating them with a lookahead
JavaScript supports neither atomic groups nor possessive quantifiers. The standard substitute is (?=(X))\1, which works because a lookahead that has already succeeded is never retried.
At a glance
- Native atomic group
- not available, (?>...) is a syntax error
- Possessive quantifiers
- not available, a*+ is a syntax error
- Emulation
- (?=(X))\1
- Why it works
- a lookahead that has succeeded is never re-run with a shorter match
- Cost
- it consumes a capture group number
- Simpler alternatives
- bound the quantifier, or use a negated character class
In detail
Engines such as PCRE, Java and Ruby let you mark a group atomic, which tells the engine that once the group has matched it may never give any of those characters back. Possessive quantifiers do the same job at the token level. Neither exists in JavaScript, and both are syntax errors here, so patterns copied from those engines fail to compile rather than misbehaving quietly.
The substitute relies on a property of lookarounds that is easy to miss: in JavaScript a lookahead is itself atomic. Once it has found a match at a position the engine will not go back and try a shorter one, so a greedy quantifier inside a lookahead is effectively frozen at its longest successful length.
That gives the idiom. Put the pattern inside a capturing group inside a lookahead, then immediately consume the captured text with a backreference. The lookahead pins the length, the backreference re-matches exactly that text, and there is no path by which the engine can reconsider a shorter alternative. /(?=(\d+))\1foo/ matches 123foo and fails fast on 123 rather than trying every shorter run of digits.
It is not free. The technique burns a group number, which shifts every later reference in the pattern and any $1 in a replacement string, and it is genuinely hard to read. Reach for it when the simpler fixes are unavailable: bounding the quantifier and swapping a dot for a negated class both solve most real cases without the extra machinery.
Examples
// Not available in JavaScript:
// (?>\d+)foo atomic group
// \d++foo possessive quantifier
const atomic = /(?=(\d+))\1foo/;
atomic.test("123foo"); // true
atomic.test("123"); // false, and it stops rather than trying 12, then 1
atomic.test("123fo"); // false// Bound the repetition:
/\d{1,10}foo/.test("123foo"); // true
// Or make the branches unable to overlap:
/"[^"]*"/.test('"abc"'); // true, nothing to backtrack intoThings that catch people out
- The emulation consumes a capture group number, so every backreference and every $1 style token after it shifts by one.
- (?>...) and a*+ are syntax errors in JavaScript rather than silent no-ops, so a pattern lifted from PCRE or Java will fail to compile until it is rewritten.
- Wrapping something in (?=(X))\1 that was never going to backtrack adds cost and confusion for nothing. Confirm the pattern is actually pathological first.
Try it: Regex Tester
Type a pattern and a test string, watch every match highlight as you go, read a plain English breakdown of each token, and check the numbered and named groups it captured.
Regex Tester
LOCAL · Browser-OnlyTest and debug regular expressions with live matching and highlights
Related Regex syntax
All regex syntax →Catastrophic backtracking: why a regex freezes a page
Nested quantifiers over overlapping characters make the work grow exponentially with input length. Here is the shape that causes it and the rewrites for it.
(?=...): positive lookahead for zero width tests
A lookahead asserts what comes next without consuming it, which is how several independent rules can all be checked from the same position.
\1: backreferences that match what a group captured
A backreference matches the exact text a group captured, not the pattern that captured it. Inside a character class the same escape means something else.
The ^ anchor: start of string, or start of line
Where the caret matches with and without the m flag, why it consumes nothing, and the entirely different job it does inside a character class.
The $ anchor: end of string, and the trailing newline question
Where $ matches in JavaScript, why it refuses to match before a trailing newline when Python and Ruby allow it, and how to write a literal dollar sign.
\A and \z in JavaScript: what to use instead
JavaScript has no \A, \z or \Z. Here is what those anchors silently turn into, and the absolute start and end assertions that survive the m flag.
Tools for this
- Regex Tester Free online regex tester - test and debug regular expressions with live matching and highlights
- Regex Playground Regex playground with live match highlighting, named groups, find and replace, and shareable links