本页面有中文版。
Cette page est disponible en français.
Esta página está disponible en español.
Guides

Regex engines compared: JavaScript, Python, Java, PCRE and RE2

Which regex features work in which engine, tested side by side: lookbehind, named groups, Unicode, atomic groups, recursion, what $ does before a newline, and which engines can freeze on a bad pattern.

Regex Hunter logo

· 6 min read

/\$\s*(\d+)/

match

  • $40
  • $ 40
  • $5

spare

  • 40
  • USD 40
  • $x

JavaScript, Python, Java and PCRE are all backtracking regex engines with nearly the same core syntax, so most everyday patterns work in all four. They differ in the details that break code when you move a pattern: how much lookbehind they allow, how named groups are written, whether \d matches non-English digits, and what $ does before a trailing newline. RE2 (used by Go, Google Sheets and BigQuery) is the odd one out: it drops backreferences and lookarounds in exchange for a guarantee that matching never takes more than linear time.

Every row below was tested on 4 November 2026, not copied from documentation: Node 22.23 for JavaScript, Python 3.12.6 (re), Java 17.0.20 (java.util.regex), PCRE2 10.44 through PHP 8.5, and RE2 through Google's google-re2 1.1 package. Newer versions may support more.

01Where you'll meet each engine

  • JavaScript: every browser, Node, Deno and TypeScript. It's also the engine behind the boards on this site.
  • Python: the built-in re module. The third-party regex module adds a lot of what re lacks.
  • Java: java.util.regex, which Kotlin and Scala use on the JVM.
  • PCRE2: PHP's preg_ functions, grep -P, nginx, and R with perl = TRUE.
  • RE2: Google's library. Go's regexp package follows its syntax and its speed guarantee, and so do Google Sheets' REGEXMATCH and BigQuery's REGEXP_CONTAINS.

02The feature table

JavaScript Python re Java PCRE2 RE2
Lookahead (?=…) Yes Yes Yes Yes No
Lookbehind (?<=…) Yes, any length Fixed width only Yes, bounded length¹ Yes, bounded length No
Backreferences \1 Yes Yes Yes Yes No
Named group (?<n>…) (?P<n>…) (?<n>…) Both Both
Named backreference \k<n> (?P=n) \k<n> Both No
Atomic group (?>…) No Yes (3.11+) Yes Yes No
Possessive a++ No Yes (3.11+) Yes Yes No
Unicode property \p{L} Yes, with the u flag No Yes Yes Yes
Script, e.g. Greek \p{Script=Greek} No \p{IsGreek} \p{Greek} \p{Greek}
\d matches ٣ (Arabic-Indic 3) No Yes No Only in UCP mode² No
Recursion (?R) No No No Yes No
Conditional (?(1)…|…) No Yes No Yes No
Inline flags (?i) No³ Yes Yes Yes Yes
^abc$ matches "abc\n" No Yes Yes Yes No
End of text \z No No, \Z means it Yes Yes Yes
Can freeze on a bad pattern Yes Yes Yes Stops at a limit⁴ No
  1. Java's documentation asks for a bounded maximum length, but Java 17 also accepted (?<=a+) in our test. Don't rely on it.
  2. PCRE2's default \d is ASCII. PHP's /u flag turns on UCP mode, which is why \d matched ٣ there.
  3. Node 22 rejects both (?i) and the scoped (?i:…). The scoped form is new in ES2025 and only works in newer runtimes.
  4. See the speed test below: PHP returned an error instead of an answer.

03The differences that break moved patterns

$ and a trailing newline

^abc$ matches "abc\n" in Python, Java and PCRE, because their $ also matches just before a final newline. In JavaScript and RE2 it doesn't. A line read from a file with its newline still attached passes validation in one language and fails in another. To mean "the very end", use \z in Java, PCRE and RE2, and \Z in Python (where \Z is the absolute end). JavaScript has neither: without the m flag, its $ already means the very end.

Lookbehind

JavaScript allows any lookbehind, including variable-length ones like (?<=\$\s*)\d+. Python's re only allows fixed width, PCRE2 a bounded length, and RE2 none at all. The portable fix is to match the prefix and capture what you want instead:

Matches4/4 as expected
/\$\s*(\d+)/

Should match2/2

  • Total: $40
  • Total: $ 40

Should not2/2

  • Total: 40
  • Total: USD 40
Try it in the Playground

Group 1 holds the number in every engine, RE2 included.

Named groups

(?<year>\d{4}) works in JavaScript, Java, PCRE2 and RE2, but Python 3.12's re rejects it and wants (?P<year>\d{4}). Backreferences to a name differ too: \k<year> in JavaScript, Java and PCRE, (?P=year) in Python. JavaScript vs Python regex has the full list for those two.

\d, \w and Unicode

In Python 3, \d and \w match digits and letters from every script by default, so \d accepts ٣. In JavaScript, Java and RE2 they're ASCII only. For a form field that must be 0-9, write [0-9] and the pattern means the same thing everywhere (in Python you can also pass re.ASCII).

For letters in any language, \p{L} works in JavaScript (with the u flag), Java, PCRE2 and RE2, but not in Python's re.

Atomic groups and possessive quantifiers

(?>a+) and a++ take as much as they can and never give it back, which is one way to stop runaway backtracking. Java and PCRE have had them for years, and Python added them in 3.11. JavaScript and RE2 don't have them; in JavaScript, a lookahead with a backreference, (?=(a+))\1, does the same job.

04Speed: which engines can freeze

The classic bad pattern is (a+)+$ against a long run of the letter a followed by !. A backtracking engine tries every way to split the run between the inner and outer + before it gives up. Here is what each engine did with 24 letters and a !:

Engine Result
JavaScript (Node 22) 11.2 seconds
Python 3.12 1.8 seconds
Java 17 Under 1 ms
PCRE2 (PHP) Gave up after 3 ms with "Backtrack limit exhausted"
RE2 Under 1 ms, even with 100,000 letters

Two things stand out. Java 17 optimizes this shape, but it's still a backtracking engine: add a backreference, as in ^(a+)+\1$, and its time went from 9 ms to 45 ms to 590 ms as the input grew from 16 to 20 to 24 letters. PHP doesn't hang, but preg_match returns false, and code that only checks for a truthy match reads that as "no match" without noticing the error.

Only RE2 guarantees linear time for every pattern, which is why it's the safe choice for patterns users type in. Catastrophic backtracking explains how to spot and fix these patterns in any engine.

05Writing a pattern that works everywhere

If a pattern has to run in more than one engine, for example on the browser and on the server, this subset behaves the same in all five:

  • Literal text, classes like [a-z0-9_], ., and the quantifiers * + ? {n,m} with their lazy versions.
  • Anchors ^ and $ with the m flag when you mean lines, or \A…\z on the server when you mean the whole text (and never feed a trailing newline to a JavaScript $).
  • Groups (…) and (?:…), and alternation |.
  • [0-9] rather than \d, if non-English digits must be rejected.
  • No lookarounds or backreferences if RE2 might run it; capture the part you want instead.

Then test the pattern in each engine it will run in. The Playground runs JavaScript's engine, the same as this page.

06Practice

The practice packs and the daily hunt use JavaScript's engine, so lookbehind and named groups are fair game there. The Regex Guide covers each feature with live examples.