Regular Expressions from Zero to Confident
Regex has a reputation for being write-only. It is not — it is a small language with a handful of rules, and building patterns incrementally makes them readable and safe.
What a regular expression is
A regular expression is a pattern that describes a set of strings. The engine reads your pattern, walks through the input, and reports where the pattern matches.
The concept comes from Stephen Kleene’s 1951 work on regular languages in automata theory. Ken Thompson implemented it in the QED editor in 1968, and it reached wide use through grep — whose name comes from the ed command g/re/p, “globally search for a regular expression and print”.
Modern regex engines have extended far beyond formal regular languages. Back-references and lookarounds make them strictly more powerful than the theory, and also strictly slower — which is the origin of the performance problems covered at the end of this guide.
Building a pattern step by step
The most effective way to learn regex is to start with the literal text and generalise one piece at a time. Suppose you want to match dates like 2026-08-02.
Step 1: literal text
2026-08-02 matches exactly that string and nothing else. Every character in a regex matches itself unless it is a metacharacter.
Step 2: generalise the digits
\d\d\d\d-\d\d-\d\d matches any date-shaped string. \d means “any digit”.
Step 3: use quantifiers
\d{4}-\d{2}-\d{2} says the same thing more clearly. {n} means “exactly n of the preceding item”.
Step 4: capture the parts you need
(\d{4})-(\d{2})-(\d{2}) puts the year, month and day into capture groups 1, 2 and 3, available as $1, $2 and $3 in a replacement.
Step 5: name them
(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2}) is self-documenting and survives reordering.
Step 6: anchor it
Adding ^ and $ requires the whole string to be a date rather than merely containing one. Which you want depends entirely on whether you are validating or extracting.
Build patterns interactively in the regex tester, which explains each component as you type.
Greedy, lazy and why your match is too long
This is the single most common source of confusion. Quantifiers are greedy: they take as much as they can, then give characters back until the rest of the pattern matches.
Input: <b>bold</b> and <i>italic</i>
Pattern: <.*> matches the ENTIRE string
Pattern: <.*?> matches <b> only.* first consumes everything to the end, then backtracks until it finds a > — which is the last one. Adding ? makes the quantifier lazy: take as little as possible and expand only if forced.
There is a third, usually better option: exclude the delimiter from the character class.
Pattern: <[^>]*> matches <b> and never needs to backtrackThis is faster and clearer than either greedy or lazy matching, because there is exactly one way for it to match. Prefer negated character classes over lazy quantifiers wherever you can.
Lookarounds: matching by context
Lookarounds assert that something exists before or after the current position without consuming it. They are how you say “a number, but only if it is followed by px” while matching only the number.
| Syntax | Meaning | Example |
|---|---|---|
| (?=x) | Followed by x | \d+(?=px) matches 24 in 24px |
| (?!x) | Not followed by x | \d+(?!px) skips 24px |
| (?<=x) | Preceded by x | (?<=\$)\d+ matches 30 in $30 |
| (?<!x) | Not preceded by x | (?<!\$)\d+ skips $30 |
A practical example: adding thousands separators to a number requires matching every position that has a multiple of three digits ahead of it, but no more digits behind:
'1234567'.replace(/\B(?=(\d{3})+(?!\d))/g, ',') // 1,234,567Lookbehind has been supported in every major browser since 2023. Safari was the last holdout.
Five mistakes everyone makes
1. Forgetting to escape metacharacters
. matches any character, not a literal dot. example.com matches exampleXcom. Escape it: example\.com. The characters needing escapes are . * + ? ^ $ { } ( ) | [ ] \ /.
2. Not anchoring a validation pattern
/\d{4}/.test('abc1234xyz') is true because the pattern only needs to appear somewhere. For validation, always anchor with ^ and $.
3. Reusing a global regex object
A regex with the g flag keeps state in lastIndex between calls to test() and exec(), so consecutive calls on the same string alternate between true and false. Create the regex fresh, or reset lastIndex, or drop g when you only need a boolean.
4. Trying to validate email properly
The RFC 5322 grammar is so permissive that a fully conforming pattern runs to hundreds of characters, and it still cannot tell you whether the mailbox exists. Use something loose such as [^@\s]+@[^@\s]+\.[a-z]{2,} and confirm by sending an email.
5. Parsing HTML
HTML permits arbitrary nesting, which regular expressions provably cannot express. Use DOMParser. Regex is fine for scanning known-simple text, not for building a parser.
Catastrophic backtracking and ReDoS
Some patterns run in exponential time on inputs that almost match. The canonical example:
/^(a+)+$/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!')Thirty a’s followed by an exclamation mark. There is no match, but the engine must try every way of partitioning those thirty characters between the inner and outer quantifiers — over a billion combinations — before it can conclude that. Add five more characters and it takes 32 times longer.
The dangerous shape is a quantifier applied to a group that itself contains a quantifier over an overlapping character set:
(a+)+ (a|aa)+ (.*,)*
(\w+\s?)* ([a-z]+)* (\s*\w+)*This is a real denial-of-service vector. Cloudflare’s global outage on 2 July 2019 was caused by a single regex containing .*.*=.* deployed to their WAF, and Stack Overflow went down for 34 minutes in July 2016 from a trailing-whitespace pattern applied to a post with 20,000 consecutive spaces.
Defences
- Never nest quantifiers over overlapping sets.
- Make alternatives mutually exclusive so only one path can match.
- Prefer negated character classes to
.*?. - Cap input length before matching.
- Never build a pattern from unvalidated user input.
- On the server, consider RE2 or a similar linear-time engine, which refuses back-references precisely to guarantee linear performance.
Keep the cheat sheet open while you work, and test suspicious patterns against deliberately awkward input.
A practical workflow
- Collect real examples. Five strings that must match and five that must not. Guessing at the shape of your data is how patterns break in production.
- Start literal, generalise gradually. Change one thing at a time and re-check both sets.
- Test the negative cases hardest. A pattern that matches everything is easy; the value is in what it rejects.
- Comment anything non-obvious. Use the
xflag where the language supports it, or a named constant with a comment where it does not. You will not remember what it does in six months. - Consider whether regex is the right tool. For structured formats — HTML, JSON, CSV, URLs — a real parser is shorter, faster and correct. Use URL parsing,
JSON.parseandDOMParserrather than patterns.
Frequently Asked Questions
What is a regular expression?
A pattern describing a set of strings, used to search, validate, extract and replace text. The syntax dates to Ken Thompson's 1968 implementation and is now available in every mainstream language.
Why does my regex match too much?
Quantifiers are greedy by default. <.*> matches from the first < to the last >. Use a lazy quantifier (.*?) or, better, a negated class ([^>]*).
How do I match a literal dot?
Escape it as \.. Unescaped, . matches any character except newline.
What is the difference between test() and match()?
test() returns a boolean. match() returns the matched text and capture groups, or null. Beware that a global regex keeps state in lastIndex between test() calls.Can regex validate an email address?
Not properly. The RFC 5322 grammar is extremely permissive and no pattern can confirm a mailbox exists. Use a loose check and send a confirmation email.
What is catastrophic backtracking?
A pattern that takes exponential time on non-matching input, caused by nesting quantifiers over overlapping character sets. It is a denial-of-service risk when input comes from users.
Should I use regex to parse JSON or HTML?
No. Both allow arbitrary nesting, which regular expressions cannot express. Use JSON.parse and DOMParser.
Sources & further reading
- MDN: Regular expressions guide — the complete JavaScript reference
- Cloudflare: the 2 July 2019 outage — a global outage caused by one backtracking regex
- Russ Cox: Regular Expression Matching — why backtracking engines are exponential and RE2 is not
- OWASP: ReDoS — the security framing and mitigation advice