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 itEngineFamily
JavaScript (browsers, Node, Deno)Irregexpbacktracking
Python reCPython’s srebacktracking
PHP preg_*, grep -P, RPCRE2backtracking
Java, Kotlin, Scalajava.util.regexbacktracking
Go regexp, Rust regexRE2 familylinear automaton
RubyOnigmobacktracking
grep, sed, awk (default modes)POSIX BRE/EREdepends on the libc
MySQL 8.0.4+ICUbacktracking
PostgreSQLSpencer AREhybrid

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

TokenMatchesWatch out
.any character except newlineJS additionally excludes \r, U+2028 and U+2029; the s flag lifts all of it
\da digitASCII 0–9 in JS, Go and default PCRE; any Unicode digit in Python 3 and .NET, Arabic-Indic ٤ included
\wletter, digit or _ASCII-only in JS even with the u flag; Unicode in Python 3 and .NET
\swhitespacethe sets differ: JS counts no-break space and U+FEFF, default PCRE does not
[abc], [^abc]one character of / not of the setmost metacharacters turn literal inside; put ] first and - first or last
[a-z]a code-point rangeranges are numeric: [A-z] silently includes [ \ ] ^ _ and the backtick
[[:alpha:]]POSIX classgrep, 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

TokenMatches atWatch out
^ $start / end of string, or of each line with min Ruby they are line anchors always, no flag involved; in Python and PCRE $ also matches before a final newline
\Aabsolute startJS does not have it
\zabsolute endPython only added the spelling \z in 3.14; before that its \Z did the same job
\Zend, before a final newline in PCRE, Perl and Javain Python \Z is the absolute end, the behavior every other engine calls \z
\bword boundaryderived from \w, so ASCII in JS and Java; inside a class it is a backspace
\Bnot a boundaryalso 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

FormMeaningWatch out
* + ? {n} {m,n}greedy: take the maximum, give back on failurean unmatched { is a literal in most flavors but an error in strict JS u mode
*? +? ?? {m,n}?lazy: take the minimum, grow on demandnot an optimisation, often slower; it re-runs the rest of the pattern after every added character
*+ ++ ?+possessive: take the maximum, never give backPCRE, Java, Python 3.11+; a syntax error in JavaScript and Go
(?>…)atomic group: possessive for a whole subpatternsame 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

FormMeaningWatch out
(…)capturing groupnumbered by opening parenthesis, left to right
(?:…)non-capturing groupuse it by default; it keeps group numbers stable
(?<name>…)named groupPython spells it (?P<name>…); PCRE accepts both plus (?'name'…)
\1, \k<name>backreference in the patternGo and Rust refuse them by design; Python’s named form is (?P=name)
(?=…) (?!…)lookahead, positive and negativezero-width; absent from Go and Rust
(?<=…) (?<!…)lookbehind, positive and negativethe least portable feature in regex, see the matrix
(?i:…)flags scoped to a groupnot in JavaScript, in any form

Flags

FlagEffectWatch out
icase-insensitivesimple case folding only: straße does not match STRASSE anywhere
m^ and $ anchor per lineRuby’s m is dotall instead, its anchors are per-line already
sdot matches newlinere.S in Python; in JS since ES2018; Ruby calls it m
xignore whitespace, allow commentsgreat for reviewable patterns; JavaScript has nothing like it
gall matches, not the firsta JS and PHP concept; Python does it per function (findall, sub); stateful in JS, see below
u, vUnicode modes (JS)u since ES2015; v since ES2024 adds set operations like [\p{L}--[aeiou]]
ysticky: match exactly at lastIndexJS 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.

FeatureJavaScriptPython rePCRE2JavaGo / Rust
Named group(?<n>…) ES2018(?P<n>…)both syntaxes(?<n>…) since 7(?P<n>…)
Lookbehindany length; Safari only since 16.4 (2023)fixed length onlyvariable up to 255 chars since 10.43 (2024), fixed beforevariable but bounded, no *none
Backreferencesyesyesyesyesnone, by design
Possessive / atomicno3.11+yesyesmoot, nothing backtracks
\A and \zno\A yes; \z spelled \Z before 3.14yesyesyes
Inline (?i)noscoped (?i:…) since 3.6; bare (?i) only at the startyesyesyes
\d \w beyond ASCIIneverdefault, re.ASCII opts outopt-in (UCP)opt-in flagnever
Worst-case runtimeexponentialexponentialexponentialexponentiallinear, 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

  1. A JavaScript /g regex is stateful. re.test(s) and re.exec(s) advance re.lastIndex past each match, so a reused global regex answers true, false, true, false on the same string. Drop the g when you only test, or use s.match(re), which resets.
  2. Java’s matches() anchors silently. "abc".matches("b") is false: the method demands a full-string match, as if \A…\z were wrapped around your pattern. Searching is Matcher.find(). Half of Java’s “regex doesn’t work” questions are this.
  3. Python’s re.match is neither search nor fullmatch. It anchors at the start and nowhere else. re.search scans, re.fullmatch (since 3.4) demands everything. Picking the wrong one of the three fails quietly in both directions.
  4. String literals eat backslashes. In MySQL, REGEXP '\d+' sends d+ 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.
  5. The shell gets the pattern before grep does. Unquoted, grep 3.14 file happily reports 3714 and 3x14, and a stray * becomes a glob before grep ever runs. Single-quote every pattern and escape the dot: grep '3\.14'.
  6. 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 \s or 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 the m flag, for editor find-and-replace. On a server, against user input, prefer trimEnd(): 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}$ with i. Version- and variant-pinned forms, plus what the digits mean, are on our UUID generator page.
  • IPv4: \d{1,3} per octet accepts 999.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-30 passes. 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.