Regex flavors,
and where they differ.
A pattern that works in your editor and fails in your code is usually not a typo. Regex is not one language. The engines disagree about lookbehind, about how you name a group, about what a dot matches, and most importantly about whether a bad pattern can hang the process.
The engines you will meet
| Engine | Used by | Backtracking |
|---|---|---|
| JavaScript | Browsers, Node | Yes |
| PCRE | PHP, nginx, many CLI tools | Yes |
| Python re | Python | Yes |
| RE2 | Go, some Rust, BigQuery | No |
That last column matters more than any syntax difference. A backtracking engine can take exponential time on a hostile input. RE2 cannot, because it refuses the features that make it possible.
Named groups
Three syntaxes for the same idea:
JavaScript, .NET (?<year>\d{4})
Python (?P<year>\d{4})
PCRE both of the above work
JavaScript reads them back from match.groups.year, Python from m.group("year"). Copying a Python pattern into JavaScript fails on the P.
Lookbehind
Lookahead is universal. Lookbehind is not. JavaScript gained it in ES2018 and supports it in variable length form, which is unusually generous. PCRE and Python both require lookbehind to be fixed length, so (?<=ab|cde) is an error in Python and legal in JavaScript. RE2 has no lookbehind at all.
If a pattern has to run in more than one engine, avoid lookbehind. It is the single most common portability failure.
Unicode, and the u flag
In JavaScript, \p{...} property escapes only work with the u flag. Without it the pattern either throws or, worse, silently treats \p as a literal p.
/\p{Script=Greek}/u matches Greek letters
/\p{Script=Greek}/ matches "p{Script=Greek}"
The u flag also changes what a dot and a character class match: with it, surrogate pairs are treated as single code points, so emoji and other astral characters behave. Turn it on unless you have a reason not to.
Flags that mean different things
The m flag makes ^ and $ match at line breaks. The s flag, called DOTALL in Python, makes the dot match a newline. People routinely reach for m when they want s.
In JavaScript the g flag also makes the regex object stateful. It keeps a lastIndex between calls, so reusing one global regex across several tests gives alternating results. This is a genuine trap:
const re = /a/g;
re.test("a"); // true
re.test("a"); // false, lastIndex moved
Catastrophic backtracking
Nested quantifiers over overlapping alternatives are the classic hazard. A pattern like (a+)+b against a long run of a with no b forces the engine to try every partition of the string, which is exponential.
The fix is not a faster machine. Make the alternatives non overlapping, anchor the pattern, use a possessive quantifier or atomic group where the engine supports them, or move to RE2 where the failure mode does not exist. If the input comes from a user, treat it as a denial of service risk and not a performance question.
A portability checklist
- No lookbehind if the pattern must be portable.
- Use
(?<name>)and translate for Python. - Always set
uin JavaScript when Unicode is in play. - Never reuse a global regex object across calls.
- Test the pattern against a long non matching string, not just a matching one.