How a Regex Engine Actually Matches
Reading a pattern top to bottom hides the thing that matters most: matching is a left-to-right scan with backtracking, not a lookup. The animation below shows the pattern (\d{3})-(\d{4}) being applied to the string 555-1234. Watch how the engine locks in the first three digits as capture group 1, consumes the literal -, then captures the last four digits as group 2 before declaring a match.
The One Concept That Fixes Most Bugs: Greedy vs Lazy
More regex bugs come from greediness than from any other single cause. By default * and + match as much as possible, then give characters back only if the rest of the pattern would otherwise fail. The diagram below shows the same input, <b>bold</b>, matched by a greedy <.*> (grabs everything) and a lazy <.*?> (stops at the first >).
? whenever a .* or .+ sits between two delimiters.
Regular expressions (regex) are patterns that describe sets of strings. They're one of the most powerful tools for searching, matching, and manipulating text. This tutorial takes you from basic syntax to practical patterns you can use immediately.
Basic Syntax
Literal Characters
Most characters match themselves. The regex cat matches the string "cat" exactly.
Metacharacters
Special characters have meaning beyond their literal value:
| Character | Meaning |
|---|---|
. | Any single character |
^ | Start of string |
$ | End of string |
* | Zero or more of preceding |
+ | One or more of preceding |
? | Zero or one of preceding |
\ | Escape special character |
To match a literal metacharacter, escape it with a backslash: \. matches a period.
Character Classes
Square brackets define a set of characters to match:
[abc] - matches a, b, or c
[a-z] - matches any lowercase letter
[A-Z] - matches any uppercase letter
[0-9] - matches any digit
[a-zA-Z] - matches any letter
[^abc] - matches anything except a, b, or c
Shorthand Classes
Common character classes have shortcuts:
\d - digit [0-9]
\D - non-digit [^0-9]
\w - word character [a-zA-Z0-9_]
\W - non-word character
\s - whitespace (space, tab, newline)
\S - non-whitespace
Quantifiers
Quantifiers specify how many times a pattern should match:
a* - zero or more a's
a+ - one or more a's
a? - zero or one a
a{3} - exactly 3 a's
a{2,4} - 2 to 4 a's
a{2,} - 2 or more a's
Greedy vs Lazy
By default, quantifiers are greedy---they match as much as possible. Add ? to make them lazy:
".*" - greedy: matches "hello" and "world" in "hello" and "world"
".*?" - lazy: matches "hello" then "world" separately
Groups and Capturing
Parentheses create groups:
(abc)+ - one or more "abc" sequences
(cat|dog) - matches "cat" or "dog"
(\d{3})-(\d{4}) - captures area code and number separately
Groups are numbered starting at 1. Use \1, \2, etc. to reference captured groups:
(\w+)\s+\1 - matches repeated words like "the the"
Non-Capturing Groups
Use (?:...) when you need grouping without capturing:
(?:https?://)?(www\.)?example\.com
Anchors and Boundaries
^ - start of string
$ - end of string
\b - word boundary
\B - non-word boundary
Examples:
^Hello - string starts with "Hello"
world$ - string ends with "world"
\bcat\b - "cat" as a whole word (not "category")
Lookahead and Lookbehind
Assert something exists (or doesn't) without including it in the match:
foo(?=bar) - "foo" followed by "bar" (matches "foo" only)
foo(?!bar) - "foo" not followed by "bar"
(?<=foo)bar - "bar" preceded by "foo" (matches "bar" only)
(?<!foo)bar - "bar" not preceded by "foo"
Practical Examples
Email Validation
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
This matches standard email formats. For production, consider using a library.
Phone Numbers
^\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})$
Matches: (555) 123-4567, 555-123-4567, 555.123.4567, 5551234567
URLs
https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/[^\s]*)?
Matches HTTP and HTTPS URLs with optional paths.
IP Addresses
\b(?:\d{1,3}\.){3}\d{1,3}\b
Basic IPv4 pattern. For strict validation, check each octet is 0-255.
Passwords (Complexity Check)
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$
Requires lowercase, uppercase, digit, special character, and 8+ length.
Common Mistakes (Symptom โ Cause โ Fix)
When a pattern "doesn't work," it almost always falls into one of these buckets. Match the symptom, apply the fix.
| Symptom | Root cause | Fix |
|---|---|---|
| Pattern matches nothing, even valid input | Backslash eaten by the string layer before regex sees it | In most languages use double backslashes ("\\d") or a raw/verbatim string (r"\d" in Python, @"\d" in C#) |
| Matches way more than intended | Greedy .* / .+ consuming past your delimiter | Make it lazy: .*? / .+?, or replace . with a negated class like [^>]* |
| Matches a substring you didn't want (e.g. "123" in "abc123") | No anchors โ regex matches anywhere | Add ^ and $ (or \b word boundaries) to pin the position |
| Dot matches things it shouldn't | . means "any character," not a literal period | Escape it: \. โ or put it in a class [.] |
| Whole-word search also hits substrings | Missing word boundaries | Wrap the term: \bcat\b won't match "category" |
| Character class behaves oddly | Metacharacters are mostly literal inside [...] | Remember [.], [*], [+] match those literal characters; only ^, -, ], \ are special in a class |
| Browser tab / server freezes on some inputs | Catastrophic backtracking from nested quantifiers like (a+)+ | Avoid overlapping quantifiers; anchor the pattern; use atomic groups or possessive quantifiers where supported |
| Works in one language, fails in another | Flavor differences (lookbehind, named groups, Unicode) | Test in the target engine; don't assume PCRE features exist in JavaScript or POSIX grep |
Build-and-Verify Checklist
Run every non-trivial pattern through this before it reaches production:
- Anchor deliberately โ decide whether you want a full-string match (
^...$) or a search-anywhere match, then add or omit anchors on purpose. - Escape every literal metacharacter โ dots, slashes, parentheses, and question marks in your target text must be backslash-escaped.
- Default to lazy between delimiters โ swap
.*for.*?(or a negated class) whenever it sits between two markers. - Test the empty, huge, and malicious input โ an empty string, a very long string, and a near-miss that could trigger backtracking.
- Confirm capture groups โ verify each
(...)captures exactly what you reference in\1or your replacement, and switch to(?:...)where you don't need the capture. - Match the target flavor โ validate the final pattern in the language or tool that will run it, not just an online tester.
Testing Your Patterns
Use our Regex Tester to:
- Write and test patterns interactively
- See matches highlighted in real-time
- View captured groups
- Get explanations of your pattern
Testing patterns before using them in code catches errors early and helps you understand exactly what your regex matches.
Quick Reference
. any character
^ start of string
$ end of string
\d digit
\w word character
\s whitespace
[abc] character class
[^abc] negated class
a* zero or more
a+ one or more
a? optional
a{n} exactly n
a{n,m} n to m times
(...) capturing group
(?:...) non-capturing group
a|b alternation
\b word boundary
(?=...) positive lookahead
(?!...) negative lookahead
Regular expressions are powerful but can be complex. Start simple, test thoroughly, and build up complexity as needed.