Regular Expressions Explained: Syntax, Flags, Patterns & How to Test Them
Everything you need to write, read, and debug regular expressions — from the first literal character to catastrophic-backtracking pitfalls.
What are regular expressions?
A regular expression (shortened to regex or regexp) is a mini-language for describing text patterns. You write a compact formula, hand it to a regex engine, and the engine scans a string reporting where — and how often — the pattern matches. One short expression like \d{4}-\d{2}-\d{2} can find every ISO date in a million-line log file in milliseconds.
Regular expressions are available in virtually every programming language — JavaScript, Python, Java, Go, Ruby, Perl, PHP — and in command-line tools like grep, sed, and most code editors. They are used for form validation, search-and-replace, log parsing, data extraction, and URL routing. The syntax differs slightly between flavours (PCRE, Python re, Java, JavaScript), but the core concepts covered in this guide apply everywhere. The Regex Tester on sourcecodestack runs the JavaScript (ECMAScript) engine — the same engine used by Node.js and every modern browser.
Core syntax building blocks
Literals and the dot
The simplest pattern is a literal character: cat matches the exact three-character sequence cat wherever it appears in the string. Most characters match themselves. The exceptions are the twelve metacharacters: . * + ? ^ $ { } [ ] ( ) | \. To match one of these literally, escape it with a backslash: \. matches a real period.
The dot . matches any single character except a newline (unless the s dotAll flag is set). Use it sparingly — a bare dot is very permissive and can cause accidental matches.
Character classes
Square brackets define a character class — a set of characters, any one of which can match at that position. [aeiou] matches any single vowel. [a-z] uses a range shorthand for all lowercase ASCII letters. A caret at the start negates the class: [^0-9] matches anything that is not a digit.
Shorthand classes cover the most common sets and work inside or outside square brackets:
\d— digit (0–9).\Dnegates it.\w— word character: [A-Za-z0-9_].\Wnegates it.\s— whitespace (space, tab, newline, carriage return, form feed).\Snegates it.
Anchors
Anchors assert a position rather than consuming a character.
^— start of the string (or start of a line with themflag).$— end of the string (or end of a line with themflag).\b— word boundary: the border between a\wcharacter and a\Wcharacter (or the start/end of the string).\Bis the non-boundary.
For example, \bcat\b matches the whole word cat but not catch or concatenate.
Quantifiers
Quantifiers specify how many times the preceding element must appear.
*— zero or more.+— one or more.?— zero or one (makes the preceding element optional).{n}— exactly n times.{n,}— n or more times.{n,m}— between n and m times (inclusive).
By default all quantifiers are greedy — they match as many characters as possible while still allowing the overall pattern to succeed. Append a ? to make them lazy (match as few as possible): .*? matches the shortest possible run of any characters.
Groups and alternation
Parentheses serve two purposes: grouping sub-patterns so a quantifier applies to the whole group, and capturing the matched text for later use.
- Capture group
(abc)— matches abc and stores the result as group 1 (numbered left to right by opening parenthesis). - Non-capturing group
(?:abc)— groups without storing. Use this when you only need grouping for a quantifier and do not need the captured text. - Named capture group
(?<name>abc)— stores the match under a named key accessible asmatch.groups.namein JavaScript.
The pipe character | is alternation — it means "this or that": cat|dog matches either cat or dog. Enclose alternation inside a group when you only want the choice to apply to part of the pattern: I like (?:cats|dogs).
Backreferences
A backreference refers back to a previously captured group within the same pattern. \1 matches the same text that group 1 captured. This is useful for finding repeated words: \b(\w+)\s+\1\b matches doubled words like the the or very very. In replacement strings, back-references use the $1 syntax (JavaScript) or \1 (most other flavours).
Lookahead and lookbehind
Lookarounds let you match text only when it is preceded or followed by another pattern — without including that surrounding text in the match itself. They are zero-width assertions: they consume no characters.
(?=...)— positive lookahead: match if followed by ...(?!...)— negative lookahead: match if NOT followed by ...(?<=...)— positive lookbehind: match if preceded by ...(?<!...)— negative lookbehind: match if NOT preceded by ...
Example: match any number followed by the word "px" without capturing "px": \d+(?=px). Given the string font-size: 16px, it matches 16 but not px.
The six JavaScript flags
Flags modify how the engine interprets the pattern. In JavaScript they are appended after the closing slash: /pattern/gi. In the Regex Tester you toggle each flag with a button.
| Flag | Name | Effect |
|---|---|---|
| g | global | Find all matches instead of stopping at the first. |
| i | ignoreCase | Make the match case-insensitive (a matches A). |
| m | multiline | ^ and $ match the start and end of each line, not just the whole string. |
| s | dotAll | The dot . matches newline characters too. |
| u | unicode | Enables full Unicode matching; required for \p{…} property escapes and correct handling of code points above U+FFFF. |
| y | sticky | Match only at the exact position stored in lastIndex; does not scan forward. |
The most commonly used combination for a global, case-insensitive search is gi. Add m when your test string contains multiple lines and you want ^/$ to match each line individually.
Capture groups vs. non-capturing groups
Every pair of plain parentheses creates a numbered capture group. For a complex pattern with many groups, those numbers pile up quickly and replacement strings like $4 become hard to maintain. Two strategies help:
- Use non-capturing groups
(?:)whenever you only need the grouping for a quantifier or alternation and do not intend to reference the matched text later. This keeps capture group indices stable and avoids allocating memory the engine would otherwise use to store the captured substring. - Use named groups
(?<year>\d{4})for the substrings you do want to reference. Named groups are self-documenting, immune to index shifts when you add new groups, and accessible by name in JavaScript viamatch.groups.yearand in replacement strings via$<year>.
The Regex Tester shows both numbered and named groups in a table beneath the matches, so you can immediately see what each pair of parentheses is capturing.
Regex syntax quick-reference
| Token | Matches | Example |
|---|---|---|
| . | Any character except newline (without s flag) | /c.t/ → 'cat', 'cut', 'c9t' |
| \d | Digit 0–9 | /\d+/ → '42', '0', '2026' |
| \w | [A-Za-z0-9_] | /\w+/ → 'hello', 'var_1' |
| \s | Whitespace character | /\s+/ → spaces, tabs, newlines |
| [abc] | Any of a, b, or c | /[aeiou]/ → vowels |
| [^abc] | Anything except a, b, or c | /[^0-9]/ → non-digits |
| ^ | Start of string (or line with m) | /^Hello/ → lines starting with Hello |
| $ | End of string (or line with m) | /!$/ → lines ending with ! |
| \b | Word boundary | /\bcat\b/ → 'cat' but not 'catch' |
| * | 0 or more (greedy) | /a*/ → '', 'a', 'aaa' |
| + | 1 or more (greedy) | /a+/ → 'a', 'aaa' |
| ? | 0 or 1 (optional) | /colou?r/ → 'color', 'colour' |
| {n,m} | Between n and m times | /\d{2,4}/ → '12', '123', '1234' |
| *? / +? / ?? | Lazy (match as few as possible) | /a+?/ on 'aaa' → first 'a' only |
| (abc) | Capture group | /(\ w+)@/ captures username |
| (?:abc) | Non-capturing group | /(?: abc)+ groups without storing |
| (?<n>abc) | Named capture group | (?<year>\d{4}) |
| a|b | Alternation (a or b) | /cat|dog/ → 'cat' or 'dog' |
| (?=...) | Positive lookahead | /\d+(?=px)/ → numbers before px |
| (?!...) | Negative lookahead | /\d+(?!px)/ → numbers not before px |
| (?<=...) | Positive lookbehind | /(?<=\$)\d+/ → digits after $ |
| (?<!...) | Negative lookbehind | /(?<!\$)\d+/ → digits not after $ |
| \1 | Backreference to group 1 | /(\ w+) \1/ → doubled words |
Common real-world patterns explained
The patterns below are widely used starting points. No single regex fits every possible edge case for a format as open-ended as an email address — treat them as sensible defaults and adjust them for your validation rules. Paste each one into the Regex Tester to explore how they behave on your own data.
Email address
^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
Breaking this down: the local part before the @ allows letters, digits, dots, underscores, percent signs, plus signs, and hyphens. After the @ comes the domain — letters, digits, dots, and hyphens — followed by a dot and a TLD of at least two letters. The anchors ^ and $ ensure the whole string must match. This pattern does not handle international domain names (IDN) or quoted local parts; for production use, send a confirmation email rather than relying solely on regex.
URL
https?:\/\/[^\s/$.?#].[^\s]*
The s? makes the scheme match both http and https. The escaped slashes are necessary in patterns that use delimiter syntax (like JavaScript literals). The host part uses a negative character class to reject whitespace and common characters that cannot start a host. Everything after is consumed up to the next whitespace. This pattern finds URLs inside prose but is intentionally loose — it will not validate whether a URL is truly reachable.
Phone number (flexible)
[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,3}[)]?[-\s\.]?[0-9]{3,4}[-\s\.]?[0-9]{3,9}
Phone formats vary enormously worldwide. This flexible pattern accepts an optional leading +, optional parentheses around the country code, and separators that can be hyphens, spaces, or dots between digit groups. If you only need North-American 10-digit numbers in standard format you can use the simpler \(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}.
ISO date (YYYY-MM-DD)
\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])
The month is constrained to 01–12 and the day to 01–31 using alternation inside the groups. Note that regex cannot validate calendar logic like "February has at most 28 or 29 days" — that requires code after the match.
Whitespace cleanup
\s+
Replace this pattern with a single space (" ") using the g flag to collapse all runs of whitespace into one. Trim the ends with ^\s+|\s+$ (replace with empty string) or just use .trim() in JavaScript for the trimming step.
Hex colour code
#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b
This matches both six-digit (#1a2b3c) and three-digit shorthand (#fff) hex colours. The alternation tries the longer form first; if that fails it tries the shorter one. The word boundary \b prevents matching an 8-digit hash as a hex colour.
How to test and debug a regular expression
Writing regex iteratively is far easier than trying to build the perfect pattern on the first attempt. A structured workflow saves time:
- Start with a concrete example. Identify two or three strings you expect to match and two or three you expect to reject. Keep them in the test string field.
- Build the pattern piece by piece. Start with the most distinctive literal or character class. Confirm it matches the first example before adding anchors or quantifiers.
- Use a live tester. The Regex Tester highlights every match inline as you type, so you immediately see when you have over- or under-matched. The capture group table shows you exactly what each pair of parentheses captured.
- Check edge cases. Add strings with unusual spacing, different cases, Unicode characters, and empty inputs.
- Test the replace string. If you are building a search-and-replace, use the Replace panel to confirm your back-references reconstruct the output correctly before putting the pattern in production code.
- Read the error message. If the pattern field shows an error banner, the engine has told you exactly where the syntax is invalid. Common causes: unescaped special characters, unbalanced parentheses, or invalid quantifier ranges like
{5,2}.
Performance and catastrophic backtracking
Most regex patterns run in microseconds. But a poorly constructed pattern can bring a server to its knees on certain inputs, a vulnerability sometimes called ReDoS (Regular Expression Denial of Service). Understanding why this happens is essential for any developer writing regex used in production.
How backtracking works
NFA-based regex engines (which includes JavaScript, PCRE, Python, and Java) try to match a pattern by advancing through the string one character at a time. When a greedy quantifier consumes as much as possible but the rest of the pattern then fails, the engine backtracks — it gives back the last character the quantifier consumed and tries again. For a well-designed pattern this happens a small number of times. For a poorly designed one, the number of attempts can be exponential in the length of the input.
The classic catastrophic example is (a+)+$ applied to a string like aaaaaaaaab. The outer + and the inner a+ can divide the as among themselves in exponentially many ways, and the engine tries each combination when the final $ fails to match the trailing b.
How to avoid it
- Eliminate ambiguity. The heart of catastrophic backtracking is two quantifiers that can both consume the same characters. Rewrite so that each quantifier consumes a distinct set of characters. Instead of
(a+)+, usea+. - Use anchors and specific character classes. Anchoring a pattern reduces the number of positions the engine needs to try. Specific classes like
[^<]("anything except this delimiter") give the engine a hard stop and avoid runaway alternation. - Prefer possessive quantifiers or atomic groups where your language supports them. JavaScript does not have possessive quantifiers, but it does support atomic groups via
(?> ... )in newer engines. These prevent the engine from giving back characters once a group has consumed them. - Set a timeout in server code. If you accept user-submitted patterns, run the match in a worker with a timeout so one malicious input cannot stall the event loop.
- Test with adversarial input. Before deploying a pattern that will run against untrusted data, try inputs like 30 repeated characters followed by a character that cannot match. If the tester feels slow, the pattern is likely vulnerable.
Using the sourcecodestack Regex Tester
The Regex Tester is a free, client-side tool. Your pattern and test text stay in your browser and are never sent to a server — particularly useful when you are working with sensitive log files, customer data, or proprietary code.
Key things the tool shows you that plain console.log does not:
- Inline match highlighting — amber highlights show exactly which characters were matched, making it easy to spot over-matching (the pattern grabbing too much) or under-matching (only part of the expected text matched).
- Numbered and named capture group table — every match appears as a row with its start index, full match, and each group value. This is invaluable when debugging a pattern with many groups.
- Real-time replace preview — type a replacement string with back-references and see the fully replaced text immediately, without writing any JavaScript.
- Match count badge — a quick green badge shows how many matches the current pattern finds, so you can instantly see whether removing a flag doubled the match count.
- Clear error messages — rather than a silent failure, any invalid pattern syntax is reported with the engine's own diagnostic message.
Summary
Regular expressions reward the time spent learning them many times over. A pattern that would take fifty lines of character-by-character code collapses to one line of regex. The key is to build them incrementally: start with literals, add character classes and quantifiers, layer in groups and anchors, and always verify against real data. Keep patterns readable by choosing named groups over numbered ones and non-capturing groups when capture is unnecessary. And whenever a pattern will run against untrusted input, test it against adversarial strings to confirm it cannot be driven into catastrophic backtracking.
Open the Regex Tester and start experimenting — seeing matches highlighted in real time as you type is the fastest way to turn regex theory into fluent practice.