Regular expressions for everyday developers
You don't need to memorize the whole grammar. A dozen constructs cover almost every regex you'll ever write — here they are, with the traps.
Regular expressions have a reputation for being write-only — easy to produce, impossible to read six months later. That reputation is half-earned. A genuinely gnarly regex is hard to read. But the regexes you write day to day — validating input, extracting a value from a log line, find-and-replace across a file — draw from a surprisingly small vocabulary. Learn maybe a dozen constructs and you can read and write the vast majority of practical patterns with confidence.
This is that dozen, plus the handful of traps that cause real bugs.
The mental model
A regular expression is a tiny pattern language for describing sets of strings.
The engine reads your input left to right and tries to match the pattern. Most
characters match themselves: the pattern cat matches the letters c-a-t in
sequence. The power comes from metacharacters — symbols that mean "any," "one
or more," "start of line," and so on.
Character classes: matching a kind of character
Instead of a literal character, you often want "any digit" or "any letter." That's a character class.
.— any single character (except newline, usually)\d— a digit;\D— a non-digit\w— a word character (letters, digits, underscore);\W— the opposite\s— whitespace (space, tab, newline);\S— non-whitespace[abc]— any one of a, b, or c[a-z]— any lowercase letter (a range)[^0-9]— any character that is not a digit (the^inside brackets negates)
So \d\d:\d\d matches a time like 09:30, and [A-Za-z]+ matches a run of
letters.
Quantifiers: how many times
Quantifiers say how many of the preceding thing to match.
*— zero or more+— one or more?— zero or one (optional){3}— exactly three{2,4}— between two and four{2,}— two or more
\d{3}-\d{4} matches 555-1234. colou?r matches both color and colour —
the ? makes the u optional.
Anchors: where in the string
Anchors match positions, not characters.
^— start of the string (or line, in multiline mode)$— end of the string (or line)\b— a word boundary (the edge between a word char and a non-word char)
The difference between \d+ and ^\d+$ is enormous: the first matches any
digits anywhere in the input (so it happily matches abc123), while ^\d+$
requires the entire string to be digits. When you're validating input,
forgetting the anchors is the number-one reason a pattern "passes" strings it
shouldn't. \bcat\b matches the word cat but not the cat inside
category — word boundaries are how you avoid matching substrings.
Groups and alternation
Parentheses ( ) group part of a pattern, both so a quantifier can apply to the
whole group and so you can capture what matched for later use.
(ab)+— one or more repetitions ofabgr(a|e)y—grayorgrey(the|is alternation, "or")(\d{4})-(\d{2})-(\d{2})— captures year, month, and day as groups 1, 2, 3
Captured groups are how you extract data, not just test for a match. In a
replacement, you refer back to them as $1, $2 (or \1, \2 depending on the
tool), so you can reformat 2026-07-03 into 07/03/2026 in one substitution.
There's also a non-capturing group, (?:...), for when you need grouping but
don't want to capture — useful for performance and for keeping your group numbers
meaningful.
The traps that cause real bugs
Knowing the syntax isn't enough; a few behaviors reliably surprise people.
Greedy vs. lazy matching
Quantifiers are greedy by default — they match as much as possible. Given the
input <a><b> and the pattern <.*>, you might expect it to match <a>. It
doesn't — .* greedily grabs everything up to the last >, matching the whole
<a><b>. Add a ? after the quantifier to make it lazy (match as little as
possible): <.*?> matches just <a>. This one behavior is behind a huge share of
"my regex matches too much" bugs.
Escaping metacharacters
To match a literal ., ?, *, (, $, or other metacharacter, escape it with
a backslash. A dot in example.com is example\.com — unescaped, that . would
match any character, so exampleXcom would pass. When you're matching things
like file names, IP addresses, or money amounts, unescaped dots are a classic
source of false matches.
Catastrophic backtracking (ReDoS)
Certain patterns, on certain inputs, can make the engine explore an exponential
number of possibilities and effectively hang. The usual culprit is nested
quantifiers over overlapping alternatives, like (a+)+$ fed a long string of
as followed by a non-match. This isn't just a performance nuisance — it's a
denial-of-service vector (ReDoS) when the pattern runs on untrusted input. If a
regex validates user-supplied data, keep it simple, avoid nesting quantifiers, and
prefer more specific character classes over broad .* runs.
Flags change everything
Most of a pattern's behavior can be toggled with flags:
i— case-insensitiveg— global (find all matches, not just the first)m— multiline (^and$match at every line, not just string ends)s— dotall (.also matches newlines)
Forgetting g on a replace is why "it only replaced the first one." Forgetting
m is why ^ didn't match the start of each line.
A worked example
Suppose you want to validate a simple email-ish string. A pragmatic pattern:
^[\w.+-]+@[\w-]+\.[A-Za-z]{2,}$
Read it piece by piece: anchored at the start, one or more word/dot/plus/hyphen
characters (the local part), an @, one or more word/hyphen characters (the
domain), a literal dot, then at least two letters (the TLD), anchored at the end.
It's not RFC-perfect — full email validation is famously near-impossible with
regex alone — but it's readable, and it rejects the obvious garbage.
Notice how the pieces you just learned combine: character classes for the allowed
characters, + quantifiers, an escaped \. for the literal dot, {2,} for the
TLD length, and ^/$ anchors so the whole string must match.
Test, don't guess
The single best habit with regex is to test against real examples — both strings that should match and strings that shouldn't — rather than reasoning purely in your head. A Regex Tester lets you paste a pattern and a block of sample text and see exactly what matches, which groups captured what, and how a replacement would come out, in real time. Watching a greedy quantifier swallow more than you intended is far more instructive than any explanation.
The takeaway
You don't need the whole regex grammar. Character classes (\d, \w, [a-z]),
quantifiers (*, +, ?, {n,m}), anchors (^, $, \b), groups, and
alternation cover almost everything you'll write. The traps to respect are greedy
matching (reach for lazy ? when it grabs too much), unescaped metacharacters
(especially the dot), and catastrophic backtracking on untrusted input. Learn the
dozen constructs, test against real strings, and regex becomes a precise tool
instead of a dark art.