JavaScript regex vs PCRE: the differences that bite

Updated 2026-08-09

The pattern you built on regex101 under its default flavor — PCRE2 — is not guaranteed to compile in Node or a browser, and the failure mode is worse than a crash: some PCRE syntax throws a SyntaxError in JavaScript, but some of it compiles and quietly matches something else. `\A` in a JS pattern is a literal letter A. `[[:alpha:]]` is a character class containing a bracket, a colon, and four letters. Neither produces an error.

The comparison targets PCRE2 as used by PHP, grep -P, and regex101's default; where a feature arrived in a specific ECMAScript edition, that edition is named so you can judge runtime support.

The compatibility table

FeaturePCRE2JavaScript
Lookahead (?=) (?!)YesYes
Lookbehind (?<=) (?<!)Fixed-length branches for most of its history; variable-length only in recent releasesES2018; fully unbounded (verified)
Named groups(?<name>...) and (?P<name>...)(?<name>...) only, ES2018 (verified)
Atomic groups (?>...)YesSyntaxError (verified)
Possessive quantifiers a++YesSyntaxError (verified)
\A \z \Z anchorsYesNo — match literal A/z/Z without the u flag (verified)
$ before a final newlineMatches (Perl-style, verified)Does not match (verified); JS $ is a true end-of-input
\G continuation anchorYesNo — use the y (sticky) flag with lastIndex (verified)
Global inline modifier (?i)YesSyntaxError (verified)
Scoped modifier (?i:...)YesES2025; works in current V8 (verified)
Recursion (?R), subroutinesYesSyntaxError (verified)
Conditionals (?(1)a|b)YesSyntaxError (verified)
POSIX classes [[:alpha:]]YesSilently parsed as a literal character class (verified)
Duplicate named groupsWith (?J) or across branchesES2025, different alternatives only (verified)
Class set operations [\p{L}--[aeiou]]NoES2024 v flag (verified)

Syntax that throws the moment you compile it

These are the good failures — you find out immediately. Atomic groups, possessive quantifiers, recursion, conditionals, and the global form of inline modifiers all fail at RegExp construction time:

node -e, verified on v25.9.0
new RegExp('(?>a+)b')      // SyntaxError: Invalid group
new RegExp('a++b')         // SyntaxError: Nothing to repeat
new RegExp('(?i)hello')    // SyntaxError: Invalid group
new RegExp('\\((?R)*\\)')   // SyntaxError: Invalid group
new RegExp('(x)?(?(1)a|b)')// SyntaxError: Invalid group

If a PCRE pattern with these constructs reaches production JavaScript, it fails loudly on first use. The dangerous cases are the ones in the next section.

Syntax that silently matches the wrong thing

JavaScript's Annex B grammar (the web-compatibility mode used when a pattern has no u or v flag) treats unknown escapes as identity escapes. PCRE's string anchors therefore degrade into literal letters, and POSIX classes parse as ordinary bracket expressions:

Verified in Node v25.9.0
/\Astart/.test('Astart')   // true  - \A is a literal 'A'
/end\z/.test('endz')       // true  - \z is a literal 'z'
/\G/.test('G')             // true  - \G is a literal 'G'
/[[:alpha:]]/.test('a')    // false - not a POSIX class
/[[:alpha:]]/.test('a]')   // true  - class {[ : a l p h} + literal ']'

The fix is mechanical: always compile with the u or v flag. Both switch the grammar to strict mode, and the same broken escapes become compile errors — verified: `new RegExp('\\Astart', 'u')` throws "Invalid escape". For the semantics you actually wanted, `\A` becomes `^` without the m flag, and `\z` becomes `$` without m. Note one asymmetry with no JS equivalent: PCRE's `$` (and `\Z`) match before a string-final newline, while JavaScript's `$` does not — verified, `/end$/.test('end\n')` is false in Node and the Perl equivalent is true. Trim input before anchoring if it may carry a trailing newline.

Lookbehind: late to arrive, now stronger than PCRE's

Lookbehind landed in ES2018 and is safe to use in current runtimes; the long holdout was Safari, which only shipped it in 16.4 (2023), so patterns destined for older WebKit still need a fallback. Verified: `'price 100'.match(/(?<=price )\d+/)[0]` returns "100". What is less known is that JavaScript's lookbehind is more general than PCRE's: it accepts fully unbounded variable-length patterns — verified, `/(?<=^a.*)b$/` matches — while PCRE2 required fixed-length lookbehind branches for most of its history and only relaxed that in recent releases (10.43). If you develop a habit of heavy variable-length lookbehind in JS, expect ports to PHP or grep -P to fail in the other direction.

Flags live outside the pattern, and \G becomes the y flag

PCRE lets a pattern carry its own options via `(?i)`; in JavaScript, flags are a separate constructor argument, and the global inline form throws. The scoped form `(?i:...)` was standardized in ES2025 and works in current V8 — verified: `(?i:hello) world` matches "HELLO world", and `(?-i:hello)` under the i flag correctly refuses "HELLO". If you target older runtimes, hoist the modifier to a flag.

PCRE's `\G` (anchor to the end of the previous match) has no pattern-level equivalent, but the sticky y flag reproduces it at the API level: a y-flagged regex matches only at exactly lastIndex. Verified: `/\d+/y` with lastIndex=2 matches "12" in "ab12cd", and with lastIndex=0 returns null rather than scanning ahead. Related and worth internalizing: a g-flagged regex object is stateful across exec calls (verified: successive execs returned "1" then "22" with lastIndex advancing to 6), which is a classic source of alternating match/no-match bugs when a g regex is stored in a constant and reused.

The v flag: set operations PCRE cannot express

ES2024's v flag (unicodeSets) adds character-class set operations — difference, intersection, and nested classes — that have no PCRE2 counterpart. Verified: `/[\p{L}--[aeiou]]/v` matches "b" and rejects "a": every Unicode letter except ASCII vowels, in one class. The v flag also implies the strict grammar, so it doubles as the lint mode from the Annex B section — verified, `\A` under v throws "Invalid escape". The cost is that v is the newest of the flags; check your runtime floor before shipping it.

Emulating possessive quantifiers and atomic groups

PCRE users reach for `a++` or `(?>a+)` to kill catastrophic backtracking. JavaScript has neither, but the lookahead-plus-backreference idiom reproduces the semantics: a lookahead's internal match cannot be backtracked into, and the backreference then consumes exactly what the lookahead captured.

Atomic emulation, verified
// PCRE:        (?>a+)b
// JavaScript:  (?=(a+))\1b

/^(?=(a+))\1ab$/.test('aaab')  // false - the group will not give back an 'a'
/^a+ab$/.test('aaab')          // true  - plain greedy quantifier backtracks

That verified false/true pair is the whole point: the emulated atomic group commits to its maximal match exactly like PCRE's would. It costs you a capture group slot and some readability, so reserve it for the quantifier-inside-quantifier patterns where backtracking actually explodes.

Test against the engine you ship

The regex tester on this site compiles your pattern with the native RegExp of the browser you are sitting in — the same engine class (V8, JavaScriptCore, SpiderMonkey) that will run your frontend code — so what matches there is what matches in production JS. It exposes g, i, and m flag toggles, lists every match with its capture groups and index, highlights matches in the test string, and previews replacements via String.replace, so $1 and $<name> substitutions behave exactly as they will in code.

Where it stops: there are no s, u, y, or v toggles in the UI, so dotAll, strict-grammar, sticky, and set-operation experiments need a devtools console instead; and because it is a JavaScript engine, it will happily accept patterns (unbounded lookbehind, `[\p{L}--[aeiou]]` if typed with flags in code) that your Python, PHP, or grep -P consumers will reject. Validate in the tester for JS targets; validate in the target engine for everything else.

Questions people ask

Why does my pattern work on regex101 but throw in Node?

regex101 defaults to the PCRE2 flavor. Switch the flavor selector to ECMAScript/JavaScript before building the pattern, or constructs like atomic groups, (?i), and \A will pass there and fail (or silently change meaning) in Node.

Does JavaScript have \A and \z?

No. Use ^ and $ without the m flag — they anchor to true start and end of input. Unlike PCRE's $, JavaScript's $ does not tolerate a trailing newline (verified), so trim input first if that matters.

How do I prevent catastrophic backtracking without possessive quantifiers?

Rewrite the hot quantifier as (?=(X))\1 to emulate an atomic group (verified to refuse backtracking), restructure nested quantifiers so alternatives cannot match the same text, or move to a linear-time engine like RE2 via a library if inputs are adversarial.

Are named capture groups portable between the two?

Mostly. (?<name>...) and \k<name> work in both (verified in JS). PCRE additionally accepts (?P<name>...) and (?P=name), which JavaScript rejects, so standardize on the (?<name>...) spelling.

Try it yourself