A Practical Regex Primer for People Who Keep Forgetting the Syntax
Regular expressions have a reputation for being write-only. The good news is that about a dozen features cover the vast majority of real-world patterns. If you learn those properly, and test as you go, regex becomes a tool you reach for rather than something you copy from a forum and hope for the best.
Every example below works in JavaScript, and nearly all of them work unchanged in Python, Go, Java and PHP. Open the regex tester in another tab and try them as you read.
Literal characters and the dot
Most characters match themselves: cat finds "cat". The dot . matches any single character except a newline. That's also the first trap: 3.14 matches "3.14", but also "3x14" and "3-14". To match a literal dot, escape it: 3\.14.
The characters that need escaping when you mean them literally are . * + ? ^ $ { } ( ) | [ ] \.
Character classes
Square brackets match one character from a set. [aeiou] matches any vowel; [0-9a-f] matches a hex digit; [^0-9] (with a caret first) matches anything that isn't a digit. There are shortcuts for the common ones:
| Shortcut | Means | Opposite |
|---|---|---|
\d | A digit | \D |
\w | Letter, digit or underscore | \W |
\s | Whitespace (space, tab, newline) | \S |
Note that \w only covers ASCII letters in JavaScript. To match accented or non-Latin letters, use the u flag and \p{L}.
Quantifiers
Quantifiers say how many times the previous item repeats.
?zero or one (optional):colou?rmatches both spellings.*zero or more.+one or more.{3}exactly three,{2,4}two to four,{2,}two or more.
By default quantifiers are greedy: they grab as much as possible and only give back what they must. Run ".+" against say "hi" and "bye" and you get one match, "hi" and "bye", from the first quote to the last. Adding a ? makes it lazy: ".+?" gives two matches. Even better is to say exactly what's allowed: "[^"]*" means "a quote, any non-quote characters, a quote", which is both correct and fast.
Anchors and boundaries
^ matches the start of the string and $ the end. With the m flag they match at the start and end of every line instead. Anchors are essential for validation: \d{5} finds five digits somewhere in "abc123456", but ^\d{5}$ only accepts a string that is exactly five digits.
\b is a word boundary, the invisible point between a word character and a non-word character. \bcat\b finds "cat" in "the cat sat" but not in "concatenate".
Groups and alternation
Parentheses do two jobs: they group things so a quantifier or | applies to the whole group, and they capture what matched so you can use it later.
(\d{4})-(\d{2})-(\d{2})
Against "2026-09-02" that captures "2026", "09" and "02" as groups 1, 2 and 3. In a replacement you can rearrange them: replacing with $3/$2/$1 turns ISO dates into day/month/year.
Named groups are much easier to read six months later:
(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})
If you only need grouping and not capturing, use (?:...). For example (?:https?|ftp):// matches any of the three schemes without creating a group. The pipe | means "or" and has the lowest precedence, so ^cat|dog$ means "starts with cat" or "ends with dog". You probably wanted ^(?:cat|dog)$.
Lookarounds
Lookarounds check what's next to the current position without including it in the match.
(?=...)lookahead:\d+(?=px)matches the number in "16px" but not the "px".(?!...)negative lookahead:^(?!.*password).*$matches lines that don't contain "password".(?<=...)lookbehind:(?<=\$)\d+matches the amount in "$250".(?<!...)negative lookbehind.
Know when to stop
Regex is great for finding and reshaping text with a regular structure. It's the wrong tool for parsing nested structures like HTML or JSON, and for fully validating things with complicated specifications. The "official" email regex runs to thousands of characters, and it still can't tell you whether the mailbox exists. For email, a loose pattern like ^[^\s@]+@[^\s@]+\.[^\s@]+$ plus a confirmation email is far more practical.
Avoid catastrophic backtracking
Patterns with nested quantifiers, like (a+)+$ or (\w+\s?)*$, can take exponential time on input that almost matches. A 30-character string can freeze a browser tab, and on a server it becomes a denial-of-service vulnerability (known as ReDoS). Cloudflare suffered a global outage in 2019 from a single badly behaved regex. The fix is to make alternatives mutually exclusive and avoid quantifying something that's already quantified. If a pattern feels slow in the tester, it'll be worse in production.
A workflow that helps
- Collect real examples, including ones that should not match.
- Build the pattern a piece at a time in a tester, checking highlights after each change.
- Anchor validation patterns with
^and$. - Leave a comment in your code explaining what the pattern is for, with an example.
Live match highlighting with capture groups, flags and ready-made patterns.
Open the Regex Tester