Regular Expressions From Scratch: A Practical Beginner's Guide
What a regex is and where it lives, the essential metacharacters, groups and captures, ready-made patterns, and the mistakes that make regex slow.
Updated on June 30, 2026 Β· 9 min read
What a regex is and where it lives
A regular expression (or regex) is a tiny language for describing text patterns. Instead of searching for the exact word "cat", you describe a rule β "three digits, a dash, then four digits" β and the regex engine finds everything that fits. It is the difference between pointing at one thing and describing a whole family of things.
You have already met regex without knowing it. It powers the advanced "Find and Replace" in VS Code, the grep command in your terminal, the form that complained about your email, and nearly every programming language: JavaScript, Python, PHP, Java, Go. The syntax is almost identical across all of them because most descend from the same grandfather, Perl (hence the name PCRE, Perl Compatible Regular Expressions).
In practice a regex does three things: it tests whether a text matches the pattern (true or false), it extracts the pieces you care about, and it replaces what matched with something else. The fastest way to learn is by experimenting: paste a pattern and a sample text into the Regex Tester and watch, in real time, what lights up and what does not.
The essential metacharacters
Most characters in a regex stand for themselves: the pattern house matches the word "house". The magic comes from metacharacters, symbols with special meaning. These are the ones you use 90% of the time:
| Symbol | Means | Example |
|---|---|---|
| . | any character (except a line break) | c.t β cat, cot, c8t |
| \d | a digit 0β9 | \d\d β 42 |
| \w | letter, digit or _ (no accents!) | \w+ β word1 |
| \s | space, tab or line break | a\sb β "a b" |
| \D \W \S | the opposite of each one above | \D β non-digit |
One detail trips up anyone working with non-English text: \w is equivalent to [A-Za-z0-9_] and does not include accented letters. In "JoΓ£o" or "cafΓ©", the "Γ£" and "Γ©" are not counted as \w. To capture words with accents, either list them by hand ([A-Za-zΓ-ΓΏ]) or switch to Unicode mode with properties like \p{L} and the u flag.
When you need the literal meaning of a metacharacter, put a backslash in front. To match a real dot β in "file.pdf" β write \., not . (which would match any character).
Classes, quantifiers and anchors
Character classes
Square brackets create a set: match any one of the characters inside them. [aeiou] matches a vowel; [a-z] uses a range; [0-9A-F] matches a hexadecimal digit. A caret right after the opening bracket flips it: [^0-9] matches anything that is not a digit.
Quantifiers
On its own, a pattern matches once. Quantifiers say how many times to repeat what comes before:
?β zero or one time (optional)*β zero or more times+β one or more times{3}β exactly 3;{2,4}β 2 to 4;{2,}β 2 or more
So \d{5}-?\d{3} reads "five digits, an optional dash, three digits". By default quantifiers are greedy: they grab as much as they can. In <b>hi</b>, the pattern <.*> matches the whole line, not just the first tag. Adding a ? makes the quantifier lazy: <.*?> stops at the earliest possible match.
Anchors
Anchors do not match characters β they match positions. The ^ marks the start of the text and $ marks the end. They are what separates "validate" from "find". Without anchors, \d{3} accepts "abc123def" because there is a run of three digits inside. With ^\d{3}$, the entire text must be exactly three digits. The \b marks a word boundary, handy for matching a whole word and not as part of another.
Groups and captures
Parentheses do two jobs at once: they group a chunk so a quantifier applies to all of it, and they capture what matched so you can reuse it later. In (ha)+, the + repeats the whole group, matching "ha", "haha", "hahaha".
The classic use is pulling out pieces. Take the pattern (\d{2})/(\d{2})/(\d{4}) applied to "30/06/2026". It creates three groups: group 1 becomes "30", group 2 becomes "06", group 3 becomes "2026". In a replacement you refer to them as $1, $2, $3. Replacing with $3-$2-$1 turns the day-first date "30/06/2026" into the ISO format "2026-06-30" in a single line.
Useful variations: (?:...) groups without capturing (faster, and it keeps numbering clean when you only need the quantifier); (?<year>\d{4}) names the group, so you access it by year instead of counting parentheses; and the vertical bar | is "or": (jpg|png|gif) matches any of the three extensions.
Ready-made patterns: email, postal code and phone
With the vocabulary above, you can build the patterns that show up most in everyday work. Drop each one into the Regex Tester and watch it match.
| What | Pattern | Matches |
|---|---|---|
| Postal code (Brazil CEP) | ^\d{5}-?\d{3}$ | 01310-100 / 01310100 |
| Phone with area code | ^\(\d{2}\)\s?9?\d{4}-?\d{4}$ | (11) 98765-4321 |
| Email (pragmatic) | ^[^\s@]+@[^\s@]+\.[^\s@]+$ | [email protected] |
Look closely at the email pattern. It does not try to list every domain on earth; it only requires "something, an at sign, something, a dot, something", with no spaces. Attempts to write the "perfect" email regex produce monsters hundreds of characters long that still reject valid addresses. For most forms, the simple pattern above already filters out gross typos β and real validation means sending a confirmation email. If you want a more robust check without hand-rolling the regex, the Email Validator handles it.
Regex is also for cleaning text, not only validating. To build a URL slug from a title, the core step is replacing everything that is not a letter or digit with a hyphen: [^a-z0-9]+ β - (after stripping accents and lowercasing). That is exactly what Slugify does: "Hello, World!" becomes "hello-world".
Mistakes that hang or slow your regex (backtracking)
Regex is powerful, but it has traps. Four cause nearly all the headaches:
- Forgetting to escape the dot. Validating "site.com" with
site.comalso accepts "siteXcom", because the.matches any character. Usesite\.com. - Validating without anchors. Without
^and$, "12345abc" passes a rule that should accept only "12345". - Being too greedy.
.*tends to swallow more than you intended; switch to a lazy version.*?or to a specific class. - Catastrophic backtracking. The most dangerous one, because it can freeze your program.
Backtracking happens when the engine has to try many combinations to decide whether something matches. Nested quantifiers are the recipe for disaster. Look at ^(\d+)+$ applied to a long string of digits ending in a letter, like "99999999999999999999X". Because the "X" will never match $, the engine tries to split the digits between the inner + and the outer one in every possible way before giving up β an exponential number of attempts. At 25 digits this already takes seconds; at 40 it hangs. The fix is simple: ^\d+$ does the same job without nesting.
This problem even has a security name: ReDoS (Regular expression Denial of Service). One badly built regex on a server can be brought down by a single malicious string. The defense is to avoid nested quantifiers ((a+)+, (.*)*), prefer specific classes over .*, and always test with long inputs in the Regex Tester before shipping to production.
Frequently asked questions
Is regex the same in every language?
Almost. The core β metacharacters, quantifiers, groups β is the same in JavaScript, Python, PHP, Java and most tools. The differences appear in advanced features: JavaScript only got lookbehind and named groups in 2018, and it has no possessive quantifiers or atomic groups like PCRE. For everyday patterns, what you learn here works everywhere.
How do I search ignoring upper and lower case?
Use the i flag (for insensitive). In JavaScript, /house/i matches "House", "HOUSE" and "house". Other handy flags: g (find every match, not just the first) and m (makes ^ and $ apply per line).
Can I use regex to validate email definitively?
No regex accepts exactly every valid email and rejects every invalid one β the specification is full of edge cases. Use a simple pattern to block obvious typos, then confirm the address for real by sending a message. Regex validation is the doorman, not the final proof.
Can I parse HTML with regex?
For one-off tasks (finding all the href values, say) it works. To truly understand a document's structure, it does not: HTML can nest in ways a regex cannot track. There the right tool is a real parser. Regex shines on "flat" text with local patterns; documents with hierarchy call for something else.
What is the difference between greedy and lazy?
Greedy (the default) grabs as many characters as possible, then "gives some back" if needed; lazy (with a ? after the quantifier) grabs the minimum and only extends when forced. In "<a><b>", <.*> matches everything, while <.*?> matches only "<a>". Picking the right one keeps you from capturing too much.
Tools mentioned in this guide
Regex Tester
Test regular expressions in real time. See highlighted matches and captured groups.
Email Validator
Check if an email address is valid. Analyzes structure, domain and local part. Processed in the browser, no data sent to any server.
Slugify
Convert titles and text into URL-friendly slugs. Removes accents, converts to lowercase and replaces spaces with hyphens. Essential for SEO.
Keep reading
JSON in Practice: Format, Validate and Convert Without the Headache
JSON syntax in five minutes, the most common errors, pretty-print vs minify, and how to convert to CSV, YAML and XML.
7 min read
Inside a JWT: The Anatomy of a Token
The three parts of a JWT, what goes (and never should go) in the payload, how the signature protects it, and why decoding is not trusting.
7 min read