Which engine actually runs your pattern
“Regex” names a family, not a language. The pattern you polished on regex101 ran on PCRE2 unless you touched the flavor picker; in production the same characters land in a different engine with different rules. Before any syntax, know which one you are talking to:
| Where you write it | Engine | Family |
|---|---|---|
| JavaScript (browsers, Node, Deno) | Irregexp | backtracking |
Python re | CPython’s sre | backtracking |
PHP preg_*, grep -P, R | PCRE2 | backtracking |
| Java, Kotlin, Scala | java.util.regex | backtracking |
Go regexp, Rust regex | RE2 family | linear automaton |
| Ruby | Onigmo | backtracking |
| grep, sed, awk (default modes) | POSIX BRE/ERE | depends on the libc |
| MySQL 8.0.4+ | ICU | backtracking |
| PostgreSQL | Spencer ARE | hybrid |
The family column decides more than the feature lists do. Linear-automaton engines (Go, Rust, RE2) refuse backreferences and lookaround outright, and in exchange they can never blow up on malicious input. Backtracking engines take the full syntax and can. MySQL is the quiet surprise in the table: 8.0.4 swapped Henry Spencer’s engine for ICU, so patterns that failed for years on old servers work now, and a few old ones changed meaning.
Characters and classes
| Token | Matches | Watch out |
|---|---|---|
. | any character except newline | JS additionally excludes \r, U+2028 and U+2029; the s flag lifts all of it |
\d | a digit | ASCII 0–9 in JS, Go and default PCRE; any Unicode digit in Python 3 and .NET, Arabic-Indic ٤ included |
\w | letter, digit or _ | ASCII-only in JS even with the u flag; Unicode in Python 3 and .NET |
\s | whitespace | the sets differ: JS counts no-break space and U+FEFF, default PCRE does not |
[abc], [^abc] | one character of / not of the set | most metacharacters turn literal inside; put ] first and - first or last |
[a-z] | a code-point range | ranges are numeric: [A-z] silently includes [ \ ] ^ _ and the backtick |
[[:alpha:]] | POSIX class | grep, sed, PostgreSQL, and inside PCRE classes; unknown to JS and Python |
\p{L} | Unicode property (here: any letter) | JS needs the u flag; Python’s re lacks it entirely, only the third-party regex module has it |
Anchors and boundaries
| Token | Matches at | Watch out |
|---|---|---|
^ $ | start / end of string, or of each line with m | in Ruby they are line anchors always, no flag involved; in Python and PCRE $ also matches before a final newline |
\A | absolute start | JS does not have it |
\z | absolute end | Python only added the spelling \z in 3.14; before that its \Z did the same job |
\Z | end, before a final newline in PCRE, Perl and Java | in Python \Z is the absolute end, the behavior every other engine calls \z |
\b | word boundary | derived from \w, so ASCII in JS and Java; inside a class it is a backspace |
\B | not a boundary | also matches between two non-word characters, e.g. inside !! |
The validation traps live in this table. Ruby first: because ^ and $ anchor lines, the input alice\ndrop everything passes a check like /^[a-z]+$/, its first line matches and that is enough. The Rails security guide has told people to anchor validations with \A and \z for over a decade, and the bug still ships. Python has a smaller version of the same hole: re.match(r'^\d+$', '42\n') succeeds because $ tolerates one trailing newline. When the exact end matters, use \Z (Python) or \z (everywhere else), or re.fullmatch, which tolerates nothing.
Quantifiers
| Form | Meaning | Watch out |
|---|---|---|
* + ? {n} {m,n} | greedy: take the maximum, give back on failure | an unmatched { is a literal in most flavors but an error in strict JS u mode |
*? +? ?? {m,n}? | lazy: take the minimum, grow on demand | not an optimisation, often slower; it re-runs the rest of the pattern after every added character |
*+ ++ ?+ | possessive: take the maximum, never give back | PCRE, Java, Python 3.11+; a syntax error in JavaScript and Go |
(?>…) | atomic group: possessive for a whole subpattern | same support list plus .NET; JavaScript needs the lookahead workaround |
One structural rule matters more than all the syntax: a quantifier wrapped around another quantifier, (a+)+ and its relatives, is how a pattern turns into a denial-of-service bug. That mechanism took down Cloudflare and Stack Overflow and has its own guide here; our ReDoS checker finds the ambiguous part of a pattern and times it against a generated attack string, in your tab.
Groups and lookaround
| Form | Meaning | Watch out |
|---|---|---|
(…) | capturing group | numbered by opening parenthesis, left to right |
(?:…) | non-capturing group | use it by default; it keeps group numbers stable |
(?<name>…) | named group | Python spells it (?P<name>…); PCRE accepts both plus (?'name'…) |
\1, \k<name> | backreference in the pattern | Go and Rust refuse them by design; Python’s named form is (?P=name) |
(?=…) (?!…) | lookahead, positive and negative | zero-width; absent from Go and Rust |
(?<=…) (?<!…) | lookbehind, positive and negative | the least portable feature in regex, see the matrix |
(?i:…) | flags scoped to a group | not in JavaScript, in any form |
Flags
| Flag | Effect | Watch out |
|---|---|---|
i | case-insensitive | simple case folding only: straße does not match STRASSE anywhere |
m | ^ and $ anchor per line | Ruby’s m is dotall instead, its anchors are per-line already |
s | dot matches newline | re.S in Python; in JS since ES2018; Ruby calls it m |
x | ignore whitespace, allow comments | great for reviewable patterns; JavaScript has nothing like it |
g | all matches, not the first | a JS and PHP concept; Python does it per function (findall, sub); stateful in JS, see below |
u, v | Unicode modes (JS) | u since ES2015; v since ES2024 adds set operations like [\p{L}--[aeiou]] |
y | sticky: match exactly at lastIndex | JS only, made for writing tokenizers |
The flavor matrix
The table we reach for every time a pattern moves between languages. Versions are load-bearing here, which is exactly what the flavor-blind cheatsheets leave out.
| Feature | JavaScript | Python re | PCRE2 | Java | Go / Rust |
|---|---|---|---|---|---|
| Named group | (?<n>…) ES2018 | (?P<n>…) | both syntaxes | (?<n>…) since 7 | (?P<n>…) |
| Lookbehind | any length; Safari only since 16.4 (2023) | fixed length only | variable up to 255 chars since 10.43 (2024), fixed before | variable but bounded, no * | none |
| Backreferences | yes | yes | yes | yes | none, by design |
| Possessive / atomic | no | 3.11+ | yes | yes | moot, nothing backtracks |
\A and \z | no | \A yes; \z spelled \Z before 3.14 | yes | yes | yes |
Inline (?i) | no | scoped (?i:…) since 3.6; bare (?i) only at the start | yes | yes | yes |
\d \w beyond ASCII | never | default, re.ASCII opts out | opt-in (UCP) | opt-in flag | never |
| Worst-case runtime | exponential | exponential | exponential | exponential | linear, guaranteed |
.NET deserves its footnote: Unicode \d and \w by default, lookbehind with no length restriction at all, a match timeout since 2012, and a NonBacktracking mode since .NET 7. It is the quiet feature leader of the backtracking family. On the other end, Perl allowed variable-length lookbehind in 5.30 with the same 255-character cap PCRE2 later copied.
Six bugs that ship every week
- A JavaScript
/gregex is stateful.re.test(s)andre.exec(s)advancere.lastIndexpast each match, so a reused global regex answerstrue, false, true, falseon the same string. Drop thegwhen you only test, or uses.match(re), which resets. - Java’s
matches()anchors silently."abc".matches("b")is false: the method demands a full-string match, as if\A…\zwere wrapped around your pattern. Searching isMatcher.find(). Half of Java’s “regex doesn’t work” questions are this. - Python’s
re.matchis neither search nor fullmatch. It anchors at the start and nowhere else.re.searchscans,re.fullmatch(since 3.4) demands everything. Picking the wrong one of the three fails quietly in both directions. - String literals eat backslashes. In MySQL,
REGEXP '\d+'sendsd+to the engine, because the string parser consumes the backslash first; you need'\\d+'. Same story in Java strings and JSON. Python raw strings and JS regex literals exist to end this, use them. - The shell gets the pattern before grep does. Unquoted,
grep 3.14 filehappily reports3714and3x14, and a stray*becomes a glob before grep ever runs. Single-quote every pattern and escape the dot:grep '3\.14'. - The pattern is fine, the text is not. Text pasted from Word, PDFs or AI chats carries no-break spaces and zero-width characters that render exactly like the characters your pattern expects, and an ASCII
\sor a literal space will not match them. Our invisible character detector names every such character in a paste, and the guide to invisible Unicode covers where they come from.
Regex vs Unicode
Everything above the ASCII line is where cheatsheets usually stop and bugs usually start. The default \w in JavaScript, Java and PCRE is [A-Za-z0-9_], nothing else. An é is a non-word character to these engines, which produces genuinely strange boundaries: /\bcafé\b/.test('café') is false, because after the é there is no word/non-word transition left to anchor on. The match fails against the exact string it was written for.
In JavaScript the honest fix is the u flag plus property classes: (?<!\p{L})café(?!\p{L}) is a Unicode-aware word boundary, spelled by hand, since u upgrades many things but never \b. Python 3 needs none of this, its \w and \b are Unicode-aware unless you ask for re.ASCII.
Case-insensitivity has the same ceiling. The i flag performs simple case folding, one code point to one code point, and German ß uppercases to the two-character SS, so /straße/i does not match STRASSE in any mainstream engine. The folding your language’s toUpperCase() does is the full version; regex engines deliberately run the cheaper one. The Kelvin sign K (U+212A) shows the opposite edge: /k/iu matches it in JavaScript, /k/i without the flag does not.
Patterns you can defend
Copy-paste patterns are where cheatsheets quietly hand you liabilities, so each of these comes with its terms and conditions.
- Trailing whitespace:
[ \t]+$with themflag, for editor find-and-replace. On a server, against user input, prefertrimEnd(): the innocent-looking anchored form is quadratic on adversarial input and once took Stack Overflow down for 34 minutes. - Email: the one browsers use for
<input type="email">, from the WHATWG HTML spec:^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$. The spec calls it a willful violation of RFC 5322, on purpose: the full RFC admits addresses no provider issues. No nested quantifiers, no backtracking ambiguity, and every browser agrees with you. - UUID:
^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$withi. Version- and variant-pinned forms, plus what the digits mean, are on our UUID generator page. - IPv4:
\d{1,3}per octet accepts999.0.0.1. The honest octet is an alternation:^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$. - ISO date:
^\d{4}-\d{2}-\d{2}$checks the shape and nothing more;2026-02-30passes. Calendar validity needs a date parser, a regex cannot count the days of February. - Semver: do not write your own; the semver spec publishes an official regex in its FAQ, prerelease and build-metadata edge cases included. What the
^and~in package.json do with those versions is a range language on top, not part of the regex.
Things that only look like regex
Two pattern languages sit right next to regex on every project and share just enough syntax to mislead. .gitignore speaks glob: * stops at slashes, ** crosses them, a leading ! renegotiates, and there are no quantifiers or groups at all, build.{js,map} means nothing to git. Our gitignore tester shows which rule catches which path and why. Cron expressions are the other neighbour: */5 * * * * borrows the star and nothing else, five fields with their own grammar, decoded by the cron expression parser.
And the classic: HTML. The 2009 Stack Overflow answer about parsing HTML with regex, the one that dissolves into Zalgo text halfway through, is a joke with a correct core. Nesting is not expressible in a regular language, so a regex can slice a known, constrained shape out of one tag on one line, and it cannot parse arbitrary markup. In a browser or Node, DOMParser and friends are one line; the moment a second angle bracket appears inside an attribute, they win.
Keep a cheatsheet for the tokens, but memorise the flavor matrix. The tokens are the easy 90 percent. Every regex story that ends in a postmortem starts in the other 10.
Regex questions the tables don’t answer
What is the difference between .* and .*? in regex?
The star is greedy: it grabs as much as it can and gives characters back only when the rest of the pattern fails. Adding the question mark makes it lazy, so it takes as little as possible and grows only on demand. On <b>bold</b>, the pattern <.*> matches the whole string while <.*?> stops at <b>. Neither is safer or faster, both backtrack; when you want “everything up to the next quote or bracket”, a negated class like "[^"]*" beats both, because it cannot cross the delimiter and leaves the engine no choices to revisit.
Why does \d not work in grep or sed?
grep and sed speak POSIX regular expressions, and POSIX has no \d shorthand, so it is read as a literal d. Write [0-9] or [[:digit:]] instead. GNU grep and sed accept \w and \s as extensions but still not \d; grep -P switches GNU grep to full PCRE, and macOS ships BSD grep, where -P does not exist at all. In basic mode (without -E) the operators +, ? and | additionally need backslashes to work as operators, which is its own class of confusion.
What does (?: ) mean in a regex?
It is a non-capturing group: it bundles part of a pattern so a quantifier or alternation can apply to it, but it does not create a numbered capture. (foo|bar)+ captures, (?:foo|bar)+ only groups. Use the non-capturing form whenever you do not need the matched text back; it keeps group numbers stable when the pattern grows and saves the engine some bookkeeping.
Which characters have to be escaped in a regex?
Outside a character class: . ^ $ * + ? ( ) [ { | and the backslash itself. Inside a class only ] \ ^ and - are special, and the dash is literal when it sits first or last. Escaping too much is not always harmless: JavaScript with the u or v flag turns unknown escapes like \q into a SyntaxError, and in POSIX tools a backslash can switch a character’s meaning on instead of off. When in doubt, escape only what the flavor documents.
How do I match a pattern across multiple lines?
Decide which of two things you need: the s flag (dotall) makes the dot match newlines so one match can span lines, while the m flag only changes where ^ and $ anchor. In Python that is re.S, JavaScript has the s flag since ES2018, and Ruby confusingly calls dotall m. Where no dotall flag exists, [\s\S] is the portable stand-in for “any character including newline”. grep works line by line and cannot do this at all; use grep -Pzo, awk or perl for multi-line matches.
Why does my regex work on regex101 but not in my code?
Usually one of three reasons. The flavor: regex101 defaults to PCRE2, which supports features your runtime may lack (Go has no lookaround at all, Python only fixed-length lookbehind), so set the flavor picker to your language before testing. String escaping: pasting a pattern into a Java, JSON or MySQL string literal halves the backslashes, \d must arrive as \\d. And flag transport: a g flag you forgot, or a missing delimiter pair in PHP, changes behavior without changing the pattern text.
How do I make only part of a regex case-insensitive?
Use a scoped inline group: (?i:png|jpe?g) matches case-insensitively while the rest of the pattern stays case-sensitive. This works in PCRE, Java, Ruby, Go and Python 3.6 or newer; Java and PCRE can also switch back off with (?-i). JavaScript supports no inline modifiers at all, so there you either apply i to the whole pattern or spell the variants out with classes like [Pp][Nn][Gg].
What is the difference between [^a] and (?!a)?
A negated class consumes exactly one character that is not in the set, so it fails at the end of the string where no character is left. A negative lookahead consumes nothing; it only asserts that what follows is not a. The two also scale differently: [^ab] rejects two single characters, while (?!ab) rejects the two-character sequence ab and happily accepts an a followed by anything else. A character class can never say “not this sequence”, only lookahead can.
Is there a correct regex for email validation?
There is a practical standard: the regex in the WHATWG HTML spec, the one every browser applies to <input type="email">. The spec itself calls it a willful violation of RFC 5322, because the full RFC grammar admits comments, quoting and whitespace that no mail provider issues. Match against the browser regex or something even looser, cap the length at 254 characters, and send a confirmation mail, which is the only check that proves the inbox exists.
What is the difference between $1 and \1 in a replacement string?
They are the same idea in different dialects. JavaScript, Java and .NET use $1 and $<name> in the replacement; sed and Python use \1, and Python additionally offers \g<1> for the case where a digit follows the reference. The whole match is $& in JavaScript, & in sed and \g<0> in Python. Mixing the dialects does not error, it silently writes a literal $1 or \1 into your output, which is why this bug survives code review.
Why does \b not match next to umlauts or accented characters?
\b is defined as the transition between \w and not-\w, and in JavaScript, Java and default PCRE, \w means ASCII [A-Za-z0-9_]. An é or ü counts as a non-word character, so the engine sees a boundary in the middle of café and none at its end; /\bcafé\b/ fails against the exact string café. Python 3 gets this right out of the box because its \w is Unicode-aware. In JavaScript the fix is the u flag plus property classes in lookarounds, e.g. (?<!\p{L})café(?!\p{L}), since even the u flag does not change \b itself.