The regex that actually comes up

Most regex you will write in a career is one of about a dozen shapes, and the regex sheet here holds them as a section of its own: 11 entries in Common Patterns, from an email address to a hex colour. The rest of the 86-entry sheet is the machinery those patterns are built from - 9 quantifier entries, 8 character-class entries, 6 lookaround entries - and one section, Common Mistakes, that explains why a pattern that looks right does the wrong thing.

This guide is the shortest useful path through it: the patterns worth copying, the two concepts that cause most of the bugs, and the one case where the honest answer is that regex is the wrong tool.

The patterns worth copying

Each of these is a real entry on the sheet with its own caveat attached, and the caveats are the point - a pattern without its limits is how validation bugs get shipped.

Start with the email one, because it is the pattern people most often try to write from scratch and the one where that is most clearly a bad idea.

Covers the vast majority of real addresses. The full RFC 5322 grammar is far larger, and the sheet's note says so rather than claiming this validates an email.

Email Address (Simple) · RX
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/

Email Address (Simple) in Regular Expressions · Common Patterns

IPv4 with each octet bounded to 0-255. The alternation order is load-bearing: 25[0-5] has to be tried before 2[0-4]\d before the general case.

IPv4 Address · RX
/\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/

IPv4 Address in Regular Expressions · Common Patterns

Both #RGB and #RRGGBB, from one {1,2} on the three-character group.

Hex Color Code · RX
/^#(?:[0-9a-fA-F]{3}){1,2}$/

Hex Color Code in Regular Expressions · Common Patterns

Captures the last extension only. .tar.gz needs an explicit pattern - the note on the entry spells that out.

File Extension Extraction · RX
/\.([a-zA-Z0-9]+)$/

File Extension Extraction in Regular Expressions · Common Patterns

The 11 entries in the regex sheet's Common Patterns section, and the caveat each one carries.
PatternWhat it does not do
Email addressValidate against RFC 5322, or confirm the address exists
URLCover every valid character; URL grammar is wider than this
IPv4 addressValidate IPv6, or check the address is routable
Phone number (US formats)Handle other countries' formats
Date, YYYY-MM-DD and MM/DD/YYYYReject 02/31/2026 - format is not validity
Username validationEnforce your own length rules without editing {2,19}
Hex colourAccept named colours, rgb() or 8-digit hex with alpha
File extensionCapture double extensions such as .tar.gz
HTML tag matchingParse nested HTML - the entry says outright that regex cannot
Whitespace trimmingAnything trim() or strip() does not already do
Number with commas and decimalsHandle European formats, where comma and period swap roles

Greedy versus lazy, which is most of your bugs

A quantifier is greedy by default: it takes as much as it can and then gives characters back only when the rest of the pattern fails. Append ? and it becomes lazy, taking as little as possible and expanding only when it has to.

The classic failure is <(.*)> against a line with two tags. Greedy .* runs to the last > on the line and swallows both tags; <(.*?)> stops at the first. The sheet's own Common Mistakes entry on this names the better fix - a negated character class such as [^<]*, which cannot cross the delimiter at all and does not depend on backtracking to stop.

The full set of both forms, from the Quantifiers section.

Greedy vs Lazy Matching · RX
Greedy: .*  .+  .?     Lazy: .*?  .+?  .??

Greedy vs Lazy Matching in Regular Expressions · Quantifiers

Why defaulting to .* is a habit worth losing.

Greedy vs Lazy Confusion · RX
Input: '<b>one</b><b>two</b>'

JS:  '<b>one</b><b>two</b>'.match(/<b>.*<\/b>/)
     // Greedy: "<b>one</b><b>two</b>"  (too much!)
JS:  '<b>one</b><b>two</b>'.match(/<b>.*?<\/b>/)
     // Lazy: "<b>one</b>"  (correct!)

Greedy vs Lazy Confusion in Regular Expressions · Common Mistakes

Anchors, boundaries, and the flag that does less than you think

^ and $ anchor to the start and end of the string. \b matches a word boundary - the position between a \w and a non-\w character, which is what makes \bcat\b match cat and not category.

The m flag is the one people reach for by mistake. It changes ^ and $ to match at line boundaries, and that is all it does. It does not make . match a newline - that is the s, or dotall, flag. The sheet carries that distinction as its own entry, because getting it wrong produces a pattern that silently matches one line of a multi-line string.

A second subtlety for anyone moving between languages: \A and \Z, the absolute string anchors that ignore multiline mode, exist in Python and not in JavaScript. The Anchors & Boundaries section marks both as Python-only.

The m flag changes the anchors and nothing else.

Multiline Flag Misconceptions · RX
/m changes ^ and $ only -- NOT the dot

Multiline Flag Misconceptions in Regular Expressions · Common Mistakes

Where a word boundary actually exists - three positions, not one.

\b Word Boundary (Detail) · RX
\b  matches the boundary between \w and \W (or string start/end)

\b Word Boundary (Detail) in Regular Expressions · Anchors & Boundaries

Lookarounds: the one advanced feature worth learning

A lookaround asserts what is around the match without consuming it. Four forms: (?=) positive lookahead, (?!) negative lookahead, (?<=) positive lookbehind and (?<!) negative lookbehind. They are zero-width, which is the property that makes them useful - the text they check does not become part of the match.

The use that justifies learning them is stacked lookaheads for validation. Each one tests a different requirement from the same starting position, and because none of them advances, you can demand four independent properties of a string before a single character is consumed.

One portability note from the sheet: lookbehind arrived in JavaScript with ES2018, so very old environments do not have it.

Four requirements checked from one position, then .{8,} consumes the string. This is the pattern most password rules are.

Stacked Lookaheads for Validation · RX
^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%]).{8,}$

Stacked Lookaheads for Validation in Regular Expressions · Lookahead & Lookbehind

Catastrophic backtracking, and when to stop

A nested quantifier over an ambiguous group can make the engine explore an exponential number of paths. The sheet lists (a+)+$, (.*a){10} and (x+x+)+y as the shapes to recognise. On a non-matching input these do not fail quickly - they hang, and on a server with user-supplied input that is a denial-of-service bug rather than a slow function.

The structural fix is to remove the ambiguity: make the inner part unable to match what the outer part matches, usually with a negated character class instead of .. Possessive quantifiers would also solve it; JavaScript has none, and Python's re gained them only in 3.11.

And there is a point at which regex is simply not the tool. The sheet's own entry sets it at 50 to 80 characters: past that, split the problem into steps or use a parser. Two cases where this is not a style preference but a correctness one are nested HTML, which regex cannot parse, and dates, where a pattern can check the shape but never that 31 February does not exist.

  • Unescaped . is the single most common mistake on the sheet - it matches any character, and domains, IPs, versions and filenames all contain literal periods.
  • ^ means start-of-string outside a character class and negation as the first character inside one.
  • A regex literal with /g keeps lastIndex between test() calls, which makes repeated calls alternate between true and false.
  • Escape both brackets for a literal [ or ]; unescaped they open a character class.
  • Past about 80 characters, reach for a parser - the Common Mistakes section says the same.

The three shapes to recognise before they reach production.

Catastrophic Backtracking · RX
Dangerous: (a+)+$  or  (.*a){10}  or  (x+x+)+y

Catastrophic Backtracking in Regular Expressions · Common Mistakes

Fine for simple, non-nested cases. The entry's own note says regex cannot reliably do more.

HTML Tag Matching · RX
/<([a-z][a-z0-9]*)\b[^>]*>(.*?)<\/\1>/i

HTML Tag Matching in Regular Expressions · Common Patterns

Questions people ask

What is the best regex for validating an email address?

A practical one, not a complete one. The pattern on the sheet covers the vast majority of real addresses; the full RFC 5322 grammar is much larger and still cannot tell you whether the mailbox exists. For that, send a confirmation message.

Why does my regex match too much?

Because the quantifier is greedy. .* takes everything it can and backtracks only when the rest of the pattern fails, so <(.*)> spans two tags on one line. Use .*?, or better a negated class such as [^<]* that cannot cross the delimiter at all.

Does the multiline flag make the dot match newlines?

No. The m flag only changes ^ and $ to match at line boundaries. Making . match a newline is the s (dotall) flag. Confusing the two is on the sheet as its own common mistake.

What is catastrophic backtracking?

An exponential blow-up in the number of paths the engine tries, caused by nested quantifiers over an ambiguous group - (a+)+$ is the canonical example. On user-supplied input it is a denial-of-service risk, not just a slow match.

Can regex parse HTML?

Not reliably, and the sheet's HTML-tag entry says so. Nested tags, self-closing tags, comments and attributes containing angle brackets all defeat it. Use a parser; keep regex for the simple non-nested case.

Where this comes from

Every code block above is rendered from this site’s own entry data at build time, with a link to the entry it came from, and every count in the prose is derived from the same file. How this cheat sheet is organized explains the structure and says what the corpus does not cover.

Keep going

More guides