Catastrophic backtracking, and why matching runs in a worker
(a+)+$ can split a run of 30 "a" characters into groups in 229 ways, and when the trailing $ fails the engine backtracks through every one of them. Each extra "a" doubles the work. This is a property of backtracking matching itself: JavaScript, Python's re and PCRE all have it, which is why RE2 and Rust's regex crate give up backreferences and lookaround to rule it out.
A JavaScript engine cannot be interrupted mid-match, so the fix is isolation: every match runs in a Web Worker with a one second budget, and a worker that overruns is terminated instead of freezing the tab. For your own pattern, remove the ambiguity: (a+)+ rewrites as plain a+. JavaScript has no atomic groups or possessive quantifiers, so rewriting the inner group is the only workaround.
Numbered and named groups
match.groups gives named captures by name, but nothing in the RegExp API says which numbered slot a name occupies. This counts opening parentheses left to right, the same rule the engine uses. A non-capturing group (?:…) and a lookaround such as (?=…) consume no number, which is why two groups often end up 1 and 3 rather than 1 and 2.
Common problems
- A group shows "did not match". It lost a
|alternation, or sat in an optional group that did not run. Its value isundefinedin JavaScript too, not an empty string. - Only one match when there should be several. Without
g, only the first is returned, matchingRegExp.exec. - "Evaluation stopped after 1 second". See catastrophic backtracking above.
Frequently asked questions
What is the difference between "g" and "y"?
Global scans ahead for the next match anywhere. Sticky only matches at the position it is already at, which is what a hand-written tokenizer wants.
Can I test a pattern written for Python or PCRE here?
Only as an approximation. JavaScript has no possessive quantifiers or atomic groups, no \A or \Z anchors, and its own rules for \w.
Why did turning on "u" make a pattern that worked before throw an error?
u makes the engine strict about escapes. A bare backslash before an ordinary letter is silently ignored without it and a syntax error with it. That catches typos, and it means a sloppy pattern stops working the moment u is on.
Why did my pattern hang the page?
It did not, and that is the point of the worker. A pattern with nested quantifiers can take exponentially long on a near-match, and there is no way to interrupt a running match. Running it on a background thread with a one second budget means a runaway pattern gets terminated instead of freezing the tab.