Regular expressions (regex) are one of the most powerful — and most dreaded — tools in a developer's toolkit. They look like line noise at first glance, but once you understand the building blocks, regex becomes an indispensable tool for validation, search-and-replace, log parsing, and data extraction.
The good news: regex is a small language. There are only a handful of concepts — literals, metacharacters, quantifiers, character classes, and groups — and every intimidating pattern you've ever seen is just those five things composed together. The syntax covered here works essentially unchanged across JavaScript, PHP, Python, and most other languages, because they all descend from the same Perl-compatible (PCRE) tradition. Learn it once and it pays off everywhere: in your editor's find-and-replace, on the command line with grep, and in your code.
The Building Blocks
Literal Characters
Most characters match themselves. The regex hello matches the literal string "hello".
Metacharacters
Metacharacters are the vocabulary of regex — characters with special meaning rather than a literal match. The shorthand classes (\d, \w, \s) and their uppercase negations do most of the day-to-day work. Two deserve extra attention. The dot matches any character, which makes it a common source of over-matching: a.c matches "abc" but also "a5c" and "a c". And \b, the word boundary, is the difference between finding "cat" as a whole word and also matching it inside "concatenate" — it matches the zero-width position between a word character and a non-word character.
. Any single character (except newline) ^ Start of string $ End of string \d Any digit (0-9) \w Any word character (a-z, A-Z, 0-9, _) \s Any whitespace (space, tab, newline) \D Any non-digit \W Any non-word character \S Any non-whitespace \b Word boundary
Quantifiers
Quantifiers control how many times the preceding token may repeat. Combined with the shorthand classes they get expressive fast: \d{4} is exactly four digits, \w+ is one or more word characters, colou?r matches both English spellings. The crucial behavior to internalize is that quantifiers are greedy — they grab as much text as possible while still allowing the overall pattern to match. Given the input <b>bold</b> and <i>italic</i>, the pattern <.*> matches the entire string, not the first tag, because .* expands to the last > it can find. Appending ? makes a quantifier lazy: <.*?> stops at the first closing bracket and matches each tag individually.
* Zero or more
+ One or more
? Zero or one (optional)
{3} Exactly 3 times
{2,5} Between 2 and 5 times
{3,} 3 or more times
Character Classes
Square brackets define a set of characters to match.
[aeiou] Match any vowel [a-z] Match any lowercase letter [A-Za-z] Match any letter [0-9] Match any digit (same as \d) [^0-9] Match anything EXCEPT a digit [a-zA-Z0-9_] Same as \w
Inside brackets, most metacharacters lose their special meaning — [.+*] matches a literal dot, plus, or asterisk with no escaping needed. Only a few characters stay special: ^ negates the class when it's the first character, - denotes a range unless placed first or last, and ] closes the class. A frequent beginner error is writing [this|that] expecting alternation; brackets match single characters, so that class just matches any one of the letters t, h, i, s, a, or the pipe symbol. For alternatives between multi-character strings, you need a group: (?:this|that).
Groups & Capturing
Parentheses do two jobs at once: they group tokens so a quantifier or alternation applies to the whole sequence, and they capture whatever they matched so you can extract it afterwards. That's what turns regex from a yes/no matcher into a parsing tool — the date example below pulls year, month, and day out of a string in one line. Named groups (?<year>) are worth the extra keystrokes in any pattern with more than two groups; six months later, match.groups.year is self-documenting while match[3] is a guessing game. When you only need grouping, use the non-capturing form (?:…) so you don't accumulate meaningless numbered captures.
// Parentheses create capturing groups
const match = "2025-07-08".match(/(\d{4})-(\d{2})-(\d{2})/);
// match[1] = "2025" (year)
// match[2] = "07" (month)
// match[3] = "08" (day)
// Named groups (more readable)
const match2 = "2025-07-08".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
// match2.groups.year = "2025"
// match2.groups.month = "07"
// Non-capturing group (grouping without capturing)
/(?:https?|ftp):\/\//
Practical Examples
Theory only sticks once you've read and written real patterns. Each example below is annotated with the reasoning behind its structure — the goal is that you could modify any of them confidently, not just paste them.
Email Validation (Basic)
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
emailRegex.test("[email protected]"); // true
emailRegex.test("not-an-email"); // false
Read it left to right: anchored start, one or more characters valid in a local part, a literal @, a domain, a literal dot (escaped), and a top-level domain of at least two letters, anchored to the end. Be aware that fully RFC-compliant email validation by regex is famously impractical — the spec allows quoted strings and other exotica. A pragmatic pattern like this one catches typos; the only real verification of an address is sending mail to it, so treat the regex as a first-line sanity check.
URL Slug Validation
This pattern is a nice study in structure: [a-z0-9]+ requires the slug to start with an alphanumeric run, and (?:-[a-z0-9]+)* allows any number of additional runs, each preceded by exactly one hyphen. That construction makes leading hyphens, trailing hyphens, and doubled hyphens all impossible without any lookarounds — cleaner than trying to forbid them explicitly.
// Only lowercase letters, numbers, and hyphens
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
slugRegex.test("my-blog-post"); // true
slugRegex.test("My Blog Post"); // false
slugRegex.test("--double-dash"); // false
Extract All URLs from Text
const text = "Visit https://example.com or http://test.org for more info."; const urls = text.match(/https?:\/\/[^\s]+/g); // ["https://example.com", "http://test.org"]
Two working parts here: https? uses the optional quantifier to accept both schemes, and the g (global) flag makes match() return every occurrence instead of stopping at the first. [^\s]+ — one or more non-whitespace characters — is a deliberately loose definition of "the rest of the URL"; it will happily swallow a trailing punctuation mark like the period after a sentence-ending link. For casual extraction that's fine; for anything stricter, trim trailing punctuation in a second pass.
Password Strength Validation
This pattern introduces lookaheads. Each (?=…) is a zero-width assertion: it peeks ahead from the current position to verify a condition without consuming any characters. Because all four lookaheads sit at the start of the string, they act as independent AND conditions — somewhere ahead there must be a lowercase letter, an uppercase letter, a digit, and a special character. Only after all four assertions pass does the final class actually match (and thereby enforce the minimum length). Without lookaheads you'd need to enumerate every ordering of the requirements, which is unmanageable.
// At least 8 chars, one uppercase, one lowercase, one digit, one special char
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
Regex in PHP
PHP's preg_ family uses the same PCRE syntax, with two surface differences: the pattern is passed as a string wrapped in delimiters (the leading and trailing /, though any matching pair of characters works), and flags go after the closing delimiter, as in '/pattern/i'. The preg_replace line below is a complete slug generator — it collapses every run of characters that isn't a lowercase letter or digit into a single hyphen. preg_match finds the first match, while preg_match_all collects every match into the $matches array, which is PHP's equivalent of JavaScript's g flag.
// Match
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
echo "Valid date format";
}
// Replace
$slug = preg_replace('/[^a-z0-9]+/', '-', strtolower($title));
// Extract all matches
preg_match_all('/\b[A-Z][a-z]+\b/', $text, $matches);
// $matches[0] contains all capitalized words
Common Mistakes
- Greedy by default:
.*matches as much as possible. Use.*?for non-greedy matching. - Forgetting to escape special characters:
.,*,+,?,(,),[,]need backslash escaping when used literally. - Not anchoring: Without
^and$, your regex may match a substring instead of the full string. - Over-engineering: If a simple
string.includes()orstring.startsWith()works, don't use regex. - Catastrophic backtracking: Nested quantifiers like
(a+)+can force the engine to try an exponential number of paths on a non-matching input, freezing your application — the basis of ReDoS (regular expression denial of service) attacks. Be especially careful with patterns applied to user-supplied input. - Parsing HTML with regex: HTML is not a regular language — nesting and attribute quoting defeat any single pattern. Use a real parser (
DOMParserin the browser,DOMDocumentin PHP) and save regex for genuinely regular text.
Practice and Next Steps
Regex fluency comes from feedback loops, not memorization. Build patterns incrementally in an interactive tester such as regex101 — it explains each token as you type, shows matches live, and lets you switch between JavaScript and PCRE flavors to see the (small) differences. Keep a personal snippet file of patterns you've actually verified, because a tested pattern is worth ten copied from a forum answer. And when a pattern grows past a line, consider whether two simpler patterns, or a split-then-match approach, would be more readable — the best regex is often the one you didn't have to write. Master the building blocks in this guide and you'll find that the intimidating patterns in the wild are just these same pieces, composed.
Try Our Free Developer Tools
Encode, decode, and format your code instantly.