Introduction to Regular Expressions — Common Patterns and Practical Examples
What Are Regular Expressions?
Regular expressions (regex) are a special notation for describing patterns in strings. They are used in virtually every situation that requires string processing, including text searching, replacement, and validation.
Nearly all programming languages support regular expressions, so once you learn them, you can apply them across JavaScript, Python, Java, PHP, Go, and more.
History of Regular Expressions
The origins of regular expressions trace back to the 1950s, when mathematician and computer scientist Stephen Kleene devised a notation for describing regular languages as part of his work on finite automata theory.
In the 1960s, Ken Thompson — who would later co-create the C programming language and Unix — applied Kleene's theory to real software, building it into the ed text editor and the search command grep (Global Regular Expression Print). This is what turned regular expressions from an academic notation into a practical, everyday tool.
In the 1980s, POSIX (Portable Operating System Interface) standardized regular expression syntax, defining two flavors: BRE (Basic Regular Expressions) and ERE (Extended Regular Expressions).
Later, the Perl language, created by Larry Wall, dramatically expanded what regular expressions could express. Named capture groups, lookaheads and lookbehinds, and lazy quantifiers — features now taken for granted in modern regex — largely trace back to Perl. This extended syntax became known as PCRE (Perl Compatible Regular Expressions) and has since been adopted by many other languages, including PHP, Python, and Ruby.
Basic Metacharacters
Regular expressions are composed of ordinary characters and metacharacters (characters with special meanings). Let's start with the basic metacharacters.
Character Classes
| Metacharacter | Meaning | Example | Matches |
|---|---|---|---|
. |
Any single character (except newline) | a.c |
abc, a1c, a-c |
\d |
Digit (0-9) | \d{3} |
123, 456 |
\D |
Non-digit | \D+ |
abc, --- |
\w |
Alphanumeric and underscore | \w+ |
hello_123 |
\W |
Non-word character | \W |
@, #, |
\s |
Whitespace | \s+ |
, \t, \n |
\S |
Non-whitespace | \S+ |
hello |
Anchors
Anchors match positions rather than characters themselves.
| Metacharacter | Meaning | Example | Description |
|---|---|---|---|
^ |
Start of line | ^Hello |
Matches "Hello" at the start |
$ |
End of line | end$ |
Matches "end" at the end |
\b |
Word boundary | \bcat\b |
Matches "cat", not "catch" |
Character Sets
Square brackets [] let you define custom character classes.
[abc] # Any of a, b, or c
[a-z] # Any lowercase letter
[A-Za-z] # Any letter (upper or lower)
[0-9] # Any digit (equivalent to \d)
[^0-9] # Any non-digit (^ means negation)
[a-zA-Z0-9_] # Alphanumeric and underscore (equivalent to \w)
Escaping Special Characters
Because . * + ? ^ $ { } [ ] ( ) | \ are all metacharacters, you need to escape them with a backslash \ whenever you want to match them literally.
# Match a literal period
3\.14 # Matches "3.14" (not "3X14")
# Match literal parentheses
\(hello\) # Matches "(hello)"
Quantifiers
Quantifiers specify how many times the preceding element should repeat.
| Quantifier | Meaning | Example | Matches |
|---|---|---|---|
* |
0 or more | ab*c |
ac, abc, abbc |
+ |
1 or more | ab+c |
abc, abbc |
? |
0 or 1 | colou?r |
color, colour |
{n} |
Exactly n | \d{4} |
2026 |
{n,} |
n or more | \d{2,} |
12, 123, 1234 |
{n,m} |
Between n and m | \d{2,4} |
12, 123, 1234 |
Greedy vs. Lazy Matching
By default, quantifiers are "greedy" — they match as many characters as possible. Adding ? after a quantifier makes it "lazy," matching as few as possible.
# Input: <div>hello</div>
<.*> # Greedy: matches <div>hello</div> entirely
<.*?> # Lazy: matches <div> only
Lazy matching is particularly useful when extracting HTML tags or quoted strings.
When to use each:
- Greedy matching — when you want to capture as much as possible, such as grabbing everything up to the last occurrence of a delimiter (e.g., the non-extension part of a file path)
- Lazy matching — when you want to stop at the first closing delimiter, such as text inside HTML tags or content between quotation marks
Grouping and Capturing
Parentheses () group patterns together and capture (extract) the matched portions.
Basic Grouping
(abc)+ # One or more repetitions of "abc"
(red|blue) # "red" or "blue"
Using Capture Groups
const pattern = /(\d{4})-(\d{2})-(\d{2})/;
const match = "2026-03-08".match(pattern);
console.log(match[1]); // "2026" (year)
console.log(match[2]); // "03" (month)
console.log(match[3]); // "08" (day)
Named Capture Groups
The (?<name>...) syntax lets you assign names to groups, greatly improving readability.
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = "2026-03-08".match(pattern);
console.log(match.groups.year); // "2026"
console.log(match.groups.month); // "03"
console.log(match.groups.day); // "08"
Backreferences
Capture groups can be referenced later in the same pattern using \1, \2, and so on — known as backreferences. They let you detect patterns where the same text repeats.
# Detect consecutive duplicate words (e.g., "the the")
\b(\w+)\s+\1\b
# Match a string that opens and closes with the same quote character
(['"]).*?\1
# Matches "hello" or 'world' (opening and closing quotes match)
Backreferences are also useful with JavaScript's replace.
// Convert a date from YYYY-MM-DD to DD/MM/YYYY
const result = "2026-03-08".replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
"$<day>/$<month>/$<year>"
);
console.log(result); // "08/03/2026"
Non-Capturing Groups
When you don't need to save the match result, (?:...) creates a non-capturing group. This offers a slight performance advantage.
(?:http|https):// # Groups the protocol part without capturing it
Lookahead and Lookbehind
Lookaheads and lookbehinds specify conditions about surrounding text without including it in the match result.
| Syntax | Name | Description |
|---|---|---|
(?=...) |
Positive lookahead | Position followed by the pattern |
(?!...) |
Negative lookahead | Position not followed by the pattern |
(?<=...) |
Positive lookbehind | Position preceded by the pattern |
(?<!...) |
Negative lookbehind | Position not preceded by the pattern |
# Match digits followed by "px" (without including "px" in the match)
\d+(?=px)
# Input: "100px 200em 300px"
# Matches: "100", "300"
# Match digits preceded by "$"
(?<=\$)\d+
# Input: "$100 200 $300"
# Matches: "100", "300"
Lookaheads and lookbehinds really prove their worth in situations where you need to match a string but exclude it whenever it's followed by something specific.
// Replace "color" with "colour", but skip the "color" inside "colorful"
const text = "This color is colorful.";
const result = text.replace(/color(?!ful)/g, "colour");
console.log(result); // "This colour is colorful."
Practical Pattern Examples
Email Address Validation
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
This pattern validates common email address formats. However, full RFC 5321 compliance would require a much more complex pattern. In practice, a simple format check combined with a confirmation email is the recommended approach.
Phone Number (US Format)
^(\+1)?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$
This pattern handles various US phone number formats with or without country code, parentheses, and different separators.
URL Validation
^https?:\/\/[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+([\/\w.-]*)*\/?$
ZIP Code (US)
^\d{5}(-\d{4})?$
Matches both "12345" and "12345-6789" formats.
IPv4 Address
^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$
This strictly validates that each octet falls within the 0–255 range. 25[0-5] covers 250–255, 2[0-4]\d covers 200–249, and [01]?\d\d? covers 0–199.
Date (YYYY-MM-DD Format)
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
This restricts the month to 01–12 and the day to 01–31. That said, accounting for the actual number of days in February or for leap years is difficult with regex alone — in practice, it's more reliable to hand date parsing off to a dedicated library.
Password Strength Check
Validates that a password is at least 8 characters long and contains at least one uppercase letter, one lowercase letter, and one digit.
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$
This pattern uses three positive lookaheads to independently check for the presence of each character type.
Regex Engine Differences Across Languages
The basic regex syntax is shared across most languages, but each engine has its own quirks and extensions worth knowing about.
| Feature | JavaScript | Python (re) |
Java | PCRE (PHP, etc.) |
|---|---|---|---|---|
| Named capture groups | (?<name>...) |
(?P<name>...) |
(?<name>...) |
(?<name>...) |
| Variable-length lookbehind | Not supported (ES2018 added fixed-length only) | Supported | Supported | Supported (PCRE2) |
Unicode property \p{L} |
Supported with the u flag |
Supported | Supported | Supported in u mode |
Possessive quantifiers a++ |
Not supported | Not supported | Supported | Supported |
Inline modifiers (?i) |
Not supported (flags only) | Supported | Supported | Supported |
| Global search | g flag |
re.findall() |
Matcher.find() loop |
preg_match_all() |
Key things to watch out for:
- JavaScript attaches flags (
g,i,m,s,u,v) outside the pattern, unlike most other languages. ES2018 added limited lookbehind support, but variable-length lookbehind is still unsupported. - Python's named capture groups use the
(?P<name>...)syntax, and you reference them later with(?P=name). - Java's
String.matches()checks whether the pattern matches the entire string, so^...$anchors aren't necessary — it behaves likefullmatchin other languages.
When porting a pattern to a different language, it's worth checking the regex cheat sheet for language-specific syntax differences.
Regular Expression Performance
Regular expressions are powerful, but a poorly designed pattern can cause serious performance problems.
Catastrophic Backtracking
Most regex engines rely on a backtracking strategy: when a match attempt fails at a given position, the engine steps back and tries a different path. This is usually fast, but certain pattern designs can cause the number of paths the engine tries to grow exponentially — a phenomenon known as catastrophic backtracking.
# A dangerous pattern: nested quantifiers
(a+)+b
# Given the input "aaaaaaaaaaaac", the engine tries an exponential
# number of combinations, causing a timeout or a frozen process
This problem is known as ReDoS (Regular Expression Denial of Service) and can become a genuine security vulnerability in web applications. Both Cloudflare and Stack Overflow have suffered production outages caused by exactly this kind of pattern in the past.
How to avoid it:
# Bad: nested quantifiers
(a+)+
# Good: atomic group (available in PCRE)
(?>a+)+
# Good: possessive quantifier (available in Java and PCRE)
a++
In environments like JavaScript, where possessive quantifiers and atomic groups aren't available, the safest approach is to redesign the pattern itself to eliminate overlapping search paths.
Performance Optimization Checklist
- Avoid unnecessary capturing: use
(?:...)for groups you don't need to extract - Prefer character classes:
[abc]is more efficient thana|b|c - Use anchors:
^and$let the engine fail fast - Be specific:
\w+or[a-z]+matches faster than a vague.+ - Break up complex patterns: instead of trying to handle everything in a single regex, split the logic into multiple steps
Use the regex tester to visualize how a pattern executes — checking the step count is a great way to spot bottlenecks.
Regular Expression Flags
Most languages let you attach flags (options) to a regex to change how it behaves.
| Flag | JavaScript | Python | Description |
|---|---|---|---|
| Case-insensitive | i |
re.IGNORECASE |
Makes [a-z] behave like [A-Za-z] |
| Multiline mode | m |
re.MULTILINE |
^ and $ match the start/end of each line |
| Dot-all mode | s |
re.DOTALL |
. also matches newline characters |
| Global | g |
— | Returns all matches (equivalent to findall) |
| Unicode | u |
— | Enables Unicode properties like \p{L} |
// The i flag makes matching case-insensitive
const pattern = /hello/i;
pattern.test("Hello World"); // true
pattern.test("HELLO"); // true
// The m flag makes ^ match the start of each line
const multiline = /^start/m;
"line1\nstart here".match(multiline); // ["start"]
Conclusion and Learning Path
Regular expressions are a versatile, all-purpose tool for text processing. Once you have a solid grasp of metacharacters, quantifiers, grouping, and lookaheads, you'll be equipped to handle most real-world text-processing challenges.
An efficient path to mastering regex:
- Start with the basic metacharacters (
.,\d,\w,\s) and quantifiers (*,+,?) - Get comfortable with capture groups and character classes
[] - Sharpen your patterns with anchors (
^,$,\b) - Tackle more advanced conditions with lookaheads and lookbehinds
- Keep performance in mind as you optimize your patterns
It might feel overwhelming at first, but building up your vocabulary one common pattern at a time will get you there. The regex tester is a great way to check pattern behavior in real time as you learn, and the regex cheat sheet is a handy reference whenever you need to look up a metacharacter or piece of syntax.
References
- ECMA-262 — Regular Expressions (the JavaScript regular expression specification)
- Python
remodule official documentation - The PCRE2 project
- Mastering Regular Expressions by Jeffrey E. F. Friedl (O'Reilly) — the definitive book on regular expressions
- RFC 5321 — Simple Mail Transfer Protocol (the email address specification)