Regex has a reputation for being cryptic, but it's mostly undeserved. Most real-world patterns are built from a small set of building blocks. Once you internalize those — character classes, quantifiers, anchors, and groups — even complex patterns become readable.
This guide focuses on the parts you'll actually use, with examples that reflect real problems.
Character Classes
Character classes let you match any character from a defined set.
Literal characters match exactly what they say: cat matches the string "cat".
The dot . matches any character except a newline. It's the wildcard. c.t matches "cat", "cut", "c3t", "c t".
Square bracket classes [...] match any one character in the set:
[aeiou] matches any single vowel
[a-z] matches any lowercase letter
[A-Za-z] matches any letter
[0-9] matches any digit
[a-zA-Z0-9_] matches word characters
Negated classes [^...] match any character NOT in the set:
[^0-9] matches any non-digit
[^aeiou] matches any non-vowel
Shorthand classes are the common ones with built-in abbreviations:
| Shorthand | Equivalent | Meaning |
|---|---|---|
\d |
[0-9] |
Any digit |
\D |
[^0-9] |
Any non-digit |
\w |
[a-zA-Z0-9_] |
Word character |
\W |
[^a-zA-Z0-9_] |
Non-word character |
\s |
[ \t\n\r\f\v] |
Whitespace |
\S |
[^ \t\n\r\f\v] |
Non-whitespace |
Anchors
Anchors don't match characters — they match positions.
^ matches the start of the string (or start of a line in multiline mode).
$ matches the end of the string (or end of a line in multiline mode).
\b matches a word boundary — the position between a \w and a \W character.
^hello matches "hello" only at the start
world$ matches "world" only at the end
^hello$ matches exactly "hello" and nothing else
\bcat\b matches "cat" in "the cat sat" but not in "concatenate"
Anchors are the difference between "find this pattern anywhere in the text" and "the entire text must match this pattern."
Quantifiers
Quantifiers control how many times the preceding element can match.
| Quantifier | Meaning |
|---|---|
* |
0 or more |
+ |
1 or more |
? |
0 or 1 (optional) |
{n} |
Exactly n times |
{n,} |
n or more times |
{n,m} |
Between n and m times |
\d+ one or more digits
\d{3} exactly 3 digits
\d{2,4} 2 to 4 digits
colou?r matches "color" or "colour" (u is optional)
Greedy vs Non-Greedy
By default, quantifiers are greedy — they match as much as possible. Add ? after a quantifier to make it non-greedy (lazy), matching as little as possible.
Given the input <b>bold</b> and <i>italic</i>:
<.+> greedy — matches the entire string "<b>bold</b> and <i>italic</i>"
<.+?> lazy — matches "<b>" then "<i>" as separate matches
This trips up a lot of people when parsing HTML or similar markup. Whenever you're matching between delimiters, reach for non-greedy quantifiers first.
Groups and Capturing
Parentheses () serve two purposes: grouping and capturing.
Grouping lets you apply a quantifier to a sequence:
(ha)+ matches "ha", "haha", "hahaha"
(abc|def) matches "abc" or "def"
Capturing groups also capture the matched text for later use (in replacements or code). Each group is numbered left to right based on its opening parenthesis.
(\d{4})-(\d{2})-(\d{2})
Applied to "2024-01-15", this captures:
- Group 1:
2024 - Group 2:
01 - Group 3:
15
In JavaScript:
const match = "2024-01-15".match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(match[1]); // "2024"
console.log(match[2]); // "01"
console.log(match[3]); // "15"
Non-capturing groups (?:...) group without capturing — useful when you want the grouping behavior but don't need to reference the match later:
(?:https?|ftp):// groups the protocol alternation without capturing it
Lookahead and Lookbehind
Lookaheads and lookbehinds are zero-width assertions — they check what's around the match without including it in the match.
Positive lookahead (?=...) — match if followed by:
\d+(?= dollars) matches a number only if followed by " dollars"
Negative lookahead (?!...) — match if NOT followed by:
\d+(?! dollars) matches a number not followed by " dollars"
Positive lookbehind (?<=...) — match if preceded by:
(?<=\$)\d+ matches digits only when preceded by a dollar sign
Negative lookbehind (?<!...) — match if NOT preceded by:
(?<!\$)\d+ matches digits not preceded by a dollar sign
They let you match precisely without consuming surrounding context — once you discover them, you'll use them constantly.
Browser support note: lookaheads work in every modern browser and Node.js. Lookbehinds ((?<=...) and (?<!...)) require Chrome 62+, Firefox 78+, or Safari 16.4+ (released March 2023). If you need to support older Safari or run regex in environments that predate these versions, avoid lookbehind and restructure the pattern to use a capturing group instead.
Flags
Flags modify how the regex engine interprets the pattern:
| Flag | Meaning |
|---|---|
g |
Global — find all matches, not just the first |
i |
Case-insensitive |
m |
Multiline — ^ and $ match line boundaries |
s |
Dotall — . matches newlines too |
In JavaScript: /pattern/gi
In Python: re.compile(r"pattern", re.IGNORECASE | re.MULTILINE)
You'll use g and i most often.
Common Real-World Patterns
Here are patterns that come up repeatedly in actual work. None of these are 100% spec-compliant for edge cases, but they handle 95% of real-world inputs:
# Email (pragmatic, not RFC 5322 compliant)
^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
# US phone number (various formats)
^\+?1?\s*[\-(.]?\d{3}[\-.)]\s*\d{3}[\-\s.]\d{4}$
# URL
https?://[^\s/$.?#][^\s]*
# Date in YYYY-MM-DD format
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
# IPv4 address
^(\d{1,3}\.){3}\d{1,3}$
# Hex color
^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$
# Slug (URL-safe string)
^[a-z0-9]+(?:-[a-z0-9]+)*$
Tips for Writing Readable Regex
Comment your complex patterns. Most languages support a verbose/comment mode where whitespace and # comments are ignored:
import re
pattern = re.compile(r"""
^ # start of string
(\d{4}) # year
- # separator
(0[1-9]|1[0-2]) # month (01-12)
- # separator
(0[1-9]|[12]\d|3[01]) # day (01-31)
$ # end of string
""", re.VERBOSE)
Build complex patterns incrementally. Test each piece separately before combining. The Regex Tester is ideal for this — you can see matches highlighted in real time as you build the pattern.
Anchor more than you think you need to. An unanchored email pattern will "match" any string that contains a valid email anywhere in it. Anchors make patterns precise.
Use raw strings in Python. The r"..." prefix avoids double-escaping backslashes, making patterns much more readable. r"\d+" instead of "\\d+".
For replacing text with regex-matched patterns, the Find and Replace tool supports regex mode for batch text transformations. For analyzing text patterns, combining regex with the Word Counter helps characterize what you're working with.
The MDN Regular Expressions guide is the best reference for JavaScript regex specifics and browser compatibility.
If you're working with structured text formats alongside regex, see JSON Basics and Syntax — many regex use cases involve extracting or validating values from structured data.
Wrapping Up
The core regex vocabulary is smaller than it looks: character classes, anchors, quantifiers, groups, and a handful of assertions. With those building blocks and a good test environment, most patterns come together quickly. The Regex Tester lets you build and verify patterns interactively — paste in your test strings, write the pattern, and see exactly what matches before it goes anywhere near production code.
FAQ
What's the right regex flavor to learn first?
JavaScript/ECMAScript regex if you're a web developer — it's the de facto baseline for browser code, JSON Schema's pattern keyword, and most cloud function platforms. PCRE (Perl-Compatible Regular Expressions) is more powerful and used by PHP, Python's regex module, and many Linux tools (grep -P). They overlap significantly; the differences are in advanced features (recursion, named modes, conditional patterns) most people don't use day-to-day.
Why does my email regex sometimes fail on valid addresses?
Because RFC 5322 (the email spec) is much more permissive than common patterns assume — it allows quoted local parts, comments, escaped special characters, and IDN domains. The famous "100% RFC-compliant email regex" is over 6,000 characters long. For practical use, your simple ^[^@]+@[^@]+\.[^@]+$ is fine; the only true validation is sending a confirmation email. Don't try to perfectly validate emails with regex.
Should I use lookbehind in 2026?
Yes — lookbehind has full support in all evergreen browsers since Safari 16.4 (March 2023), Firefox 78+, and Chrome 62+. Node.js has supported it since version 10. The patterns it enables (matching content preceded by something specific without consuming the preceding text) are often the cleanest solution. The only reason to avoid it is if you must run on Safari < 16.4 or some embedded JS engines.
What's catastrophic backtracking and how do I avoid it?
It's when a regex with nested quantifiers tries every possible combination of matches before failing on a non-matching input — exponential time complexity. Classic example: (a+)+b on input aaaaaaaaaa! takes 2^n combinations to fail. Avoid by: using atomic groups (?>a+), possessive quantifiers a++ (where supported), or restructuring to make alternatives mutually exclusive. Tools like regex101 and regexploit flag dangerous patterns.
When should I use a parser instead of regex?
Whenever you're matching nested or recursive structures (HTML tags with arbitrary nesting, balanced parentheses, JSON), or when the format has its own parser. Regex is for "regular languages" — flat patterns. Parsing nested HTML with regex is a punchline because it's literally impossible (the grammar isn't regular). Use DOMParser, BeautifulSoup, cheerio, or whatever's idiomatic for your language.
How does the `g` flag interact with `match` and `replace`?
In JavaScript, the g flag changes behavior in subtle ways. String.match(/pattern/g) returns an array of all matches as strings, with no capture group access. Without g, it returns the first match with capture groups. String.replace(/pattern/g, replacement) replaces all occurrences; without g, only the first. Use String.matchAll() to get all matches with their capture groups (always requires g flag). For state-aware iteration, RegExp.exec() with g advances lastIndex between calls.
Can regex be safely run on user input?
Yes, but with care. ReDoS (Regular Expression Denial of Service) is a real attack vector — a malicious user can submit input that triggers catastrophic backtracking, freezing your server. Defenses: timeout regex execution (re.search with re.TIMEOUT in Python's regex module, vm.runInNewContext with timeout in Node), use linear-time engines like RE2 (Google's, used by Go's regexp package), and avoid regex on attacker-controlled input where possible.
What about Unicode in regex?
Tricky. \d in default mode matches only ASCII [0-9]; with the Unicode flag (u in JavaScript, re.UNICODE in Python — default in Python 3), it matches all Unicode digit characters. \w similarly expands. For matching letters across scripts, use \p{Letter} (Unicode property escapes — supported in JS with the u flag, Python's regex package, and PCRE). For emoji, you typically need \p{Extended_Pictographic} plus zero-width joiner handling — emoji parsing is its own rabbit hole.