Regular expressions (regex) are a compact language for describing text patterns. They power search-and-replace in editors, input validation, log parsing and routing in web frameworks. The syntax looks dense, but it is built from a small set of pieces. This guide covers the regex basics every developer needs, with examples you can paste into the regex tester and experiment with as you read.
Regex basics: literals and metacharacters
Most characters match themselves: the pattern cat finds “cat” in “concatenate”. A handful of metacharacters have special meaning: . ^ $ * + ? ( ) [ ] { } | \. To match one of them literally, escape it with a backslash — \. matches a dot and \$ a dollar sign. When a pattern has to match user-supplied text literally, escape it programmatically, or use the regex escape tool, rather than by hand.
Character classes
A character class matches one character from a set:
[aeiou]— any vowel;[^aeiou]— any character that is not a vowel.[a-z0-9]— ranges.\ddigit,\wword character (letters, digits and underscore),\swhitespace. The uppercase versions\D,\Wand\Sare the opposites..— any character except a line break (unless the “dot-all”sflag is on).
Note that in some engines \d and \w can match non-ASCII digits and letters in Unicode mode. Use [0-9] when you need ASCII digits only.
Quantifiers: how many times
| Quantifier | Meaning | Example |
|---|---|---|
* | 0 or more | ab* matches “a”, “ab”, “abbb” |
+ | 1 or more | \d+ matches “7” or “2024” |
? | 0 or 1 (optional) | colou?r matches both spellings |
{n} | exactly n | \d{4} matches a four-digit year |
{n,m} | between n and m | [a-z]{3,8} |
Quantifiers are greedy by default: they take as much as they can. Given <b>bold</b>, the pattern <.+> matches the entire string. Add ? to make a quantifier lazy: <.+?> matches just <b>. Better still, be specific: <[^>]+> says exactly what may appear between the brackets.
Anchors and boundaries
^— start of the string (or of each line with themflag).$— end of the string (or line).\b— a word boundary:\bcat\bmatches “cat” but not “concatenate”.
Anchors matter most in validation. \d{4} finds four digits anywhere in “abc12345”; ^\d{4}$ only accepts a string that is exactly four digits.
Groups, alternation and backreferences
Parentheses group parts of a pattern so a quantifier applies to all of them, and capture what they matched:
(\d{4})-(\d{2})-(\d{2}) captures year, month and day
(?:https?|ftp):// groups without capturing
(?<year>\d{4}) a named group (JavaScript, .NET, PCRE, Java)
(?P<year>\d{4}) the named-group syntax in Python’s re module
The pipe means “or”: cat|dog. Keep alternation inside a group when it is part of a larger pattern — ^(cat|dog)s?$, not ^cat|dogs?$, which means “starts with cat” or “ends with dog/dogs”. A backreference such as \1 matches the same text a group captured earlier, so \b(\w+)\s+\1\b finds accidentally repeated words like “the the”.
Lookahead and lookbehind
Lookarounds check what comes before or after a position without including it in the match:
\d+(?= USD)— digits followed by “ USD” (the currency is not part of the match).(?<=\$)\d+— digits preceded by a dollar sign.^(?!.*password).*$— lines that do not contain “password”.
Lookbehind has been available in JavaScript since ES2018, but not every engine supports it; Go’s RE2 supports neither lookahead nor lookbehind.
Flags
Flags change how the whole pattern behaves. In JavaScript they follow the closing slash, as in /error/gi:
i— case-insensitive.g— find all matches, not just the first (JavaScript).m—^and$match at line breaks.s—.also matches line breaks.u— Unicode mode in JavaScript, required for\p{…}property escapes.
Practical patterns to adapt
^\d{4}-\d{2}-\d{2}$ ISO date shape (2024-05-31); check the values separately
^#(?:[0-9a-fA-F]{3}){1,2}$ CSS hex colour (#fff or #0f766e)
^[^\s@]+@[^\s@]+\.[^\s@]+$ loose email sanity check
^[a-z0-9]+(?:-[a-z0-9]+)*$ URL slug (json-to-csv)
(\w+)=("[^"]*"|\S+) key=value pairs, values optionally quoted
Each pattern checks shape, not meaning: the date pattern accepts 2024-13-45. Validate values with real date or number parsing afterwards. If a pattern is hard to read, paste it into the regex explainer for a token-by-token description, and use random data from regex to generate test strings that match it.
A worked example: parsing log lines
Suppose your web server writes lines like 2024-05-31 14:02:11 ERROR [payments] Timeout after 3000 ms and you want the timestamp, level, component and message. Build the pattern one piece at a time, testing after each step:
^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (DEBUG|INFO|WARN|ERROR) \[([\w-]+)\] (.*)$
The first group captures the timestamp, the alternation captures only the levels you expect, \[([\w-]+)\] captures the component between literal square brackets, and (.*)$ takes the rest of the line. Anchoring both ends means a malformed line fails to match instead of matching partially. In JavaScript you would loop over text.matchAll(pattern) with the g and m flags; in Python, re.finditer(pattern, text, re.MULTILINE) gives you a match object per line, and match.group(2) returns the level.
Two habits make patterns like this maintainable. First, prefer named groups for anything with more than two captures, so code reads match.groups.level rather than a numbered index that silently shifts when someone adds a group. Second, keep a handful of real sample lines — including awkward ones — as test cases next to the pattern, and run them whenever it changes.
Escaping inside string literals
Most bugs where “the regex works in the tester but not in my code” are escaping bugs. In Java, C#, JSON and ordinary JavaScript or Python strings, the backslash is also the string escape character, so the regex \d+\.\d+ must be written as "\\d+\\.\\d+". Use the language’s raw-string or regex-literal syntax where available: /\d+\.\d+/ in JavaScript, r"\d+\.\d+" in Python.
Performance: avoid catastrophic backtracking
Most regex engines in JavaScript, Python, Java and PHP use backtracking. Patterns with nested quantifiers over the same characters, such as ^(a+)+$ or ^(\w+\s?)*$, can take exponential time on input that almost matches — a long run of “a” followed by “!”. On a server this is a denial-of-service risk known as ReDoS. To stay safe:
- Avoid quantifiers nested inside quantified groups when the inner and outer parts can match the same text.
- Prefer specific classes (
[^,]*) over.*. - Limit input length before matching untrusted text.
- Consider a linear-time engine such as RE2 for user-supplied patterns.
When not to use regex
Regex is ideal for flat, predictable text: identifiers, log lines, simple formats. It is the wrong tool for nested or context-dependent formats such as HTML, JSON or programming languages — use a real parser. And when a pattern grows beyond one line, a few lines of ordinary code are often clearer and easier to test.