A regular expression (regex) is a sequence of characters that defines a search pattern. Regex is used in almost every programming language for searching, validating, and transforming text. Testing regex interactively — before embedding it in code — prevents bugs that are notoriously hard to debug later.
Regex basics
| Pattern | Matches |
|---|---|
. |
Any single character |
\d |
Any digit (0–9) |
\w |
Word character (a-z, A-Z, 0-9, _) |
\s |
Whitespace (space, tab, newline) |
^ |
Start of line |
$ |
End of line |
* |
0 or more of preceding |
+ |
1 or more of preceding |
? |
0 or 1 of preceding |
{n,m} |
Between n and m of preceding |
[abc] |
Any of a, b, or c |
[^abc] |
Not a, b, or c |
(abc) |
Capture group |
a|b |
a or b |
Common regex flags
g— Global: find all matches, not just the firsti— Case insensitive:[a-z]also matches[A-Z]m— Multiline:^and$match start/end of each lines— Dotall:.matches newline characters too
Useful regex patterns
Email validation:
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
URL matching:
https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}
Phone number (flexible):
[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}
Date (YYYY-MM-DD):
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
IPv4 address:
^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$
How to test regex online
- Go to Regex Tester
- Enter your regular expression in the pattern field
- Set flags (g, i, m) as needed
- Paste or type test text in the input area
- Matches are highlighted instantly; capture groups shown below
Regex pitfalls to avoid
- Catastrophic backtracking — nested quantifiers like
(a+)+can freeze on long inputs; test with worst-case strings - Anchors — forgetting
^and$lets partial matches pass validation - Greedy vs lazy —
.*is greedy (takes as much as possible);.*?is lazy (takes as little as possible)
Catastrophic backtracking, concretely
This is the pitfall most likely to take down a production service, so it is worth seeing in action. The pattern ^(a+)+$ looks harmless:
Input: aaaaaaaaaaaaaaaaaaaaaaaaX (24 a's, then a non-match)
Because a+ can be split among the repetitions in exponentially many ways, the engine tries roughly 2^24 combinations before concluding there is no match. Add one more a and the time doubles. This is the mechanism behind ReDoS (Regular Expression Denial of Service) — a single crafted input pins a CPU core.
The warning sign is a quantifier applied to a group that already contains a quantifier, where the inner parts can match the same text: (a+)+, (a*)*, (\d+|\w+)*.
Fixes:
- Make the inner pattern exact:
^a+$matches the same strings with no ambiguity. - Use an atomic group or possessive quantifier if your engine has them —
(?>a+)+works in PCRE (PHP, Perl) and .NET, but not in JavaScript: nativeRegExpthrowsInvalid groupon it, and that is the engine this site's tester uses. In JS, rewrite the pattern instead. - Bound the input length before matching.
- Never run a user-supplied regex against a user-supplied string on a server.
Greedy vs lazy, shown
The difference is easiest to see on HTML-ish text:
Text: <b>bold</b> and <i>italic</i>
Greedy: <.+> -> matches the ENTIRE string
Lazy: <.+?> -> matches <b>, then </b>, then <i>, then </i>
Greedy takes everything to the last >, then backs off until the pattern fits. Lazy takes the minimum and expands only as needed. When a pattern "matches too much", greediness is usually why.
Why email regex is a trap
The email pattern above is fine for a signup form, but it is not RFC-correct — the actual grammar allows quoted strings, comments, and IP-literal domains, and a fully compliant expression runs to thousands of characters. Meanwhile the pattern above rejects valid addresses containing + in some variants and accepts plenty of addresses that do not exist.
The reliable check is: pattern-match loosely to catch typos, then send a confirmation email. That is the only test that proves an address is real.
Escaping user input
If any part of a pattern comes from user input, escape it. Otherwise a stray ( throws a syntax error and a .* changes the meaning:
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const re = new RegExp(escape(userInput), 'i')
The g flag and lastIndex
A regex with g is stateful. Reusing the same object across calls to .test() produces alternating true/false results, because lastIndex advances between calls:
const re = /a/g
re.test('a') // true
re.test('a') // false <- same input, different answer
Create the regex inside the loop, or reset re.lastIndex = 0, or drop g when you only need a boolean.
Frequently asked questions
Is regex the same in every language?
No. The core syntax is shared, but lookbehind, named groups, and Unicode handling vary. JavaScript, PCRE (PHP/Perl), Python re, and Go's RE2 all differ. RE2 notably has no backtracking at all, so it cannot suffer ReDoS but also does not support lookaround.
When should I not use regex?
For nested structures — HTML, JSON, source code. Regular expressions cannot count arbitrary nesting depth, so a pattern that appears to work will fail on the first deeply nested input. Use a real parser instead.
Why does my pattern work here but fail in code?
Usually double-escaping. In a string literal, \d must be written "\\d". Using a regex literal (/\d/) or a raw string (r"\d" in Python) avoids the problem.
How do I match across multiple lines?
Two different flags: m makes ^ and $ match at each line break, while s makes . match a newline. They are independent and often both needed.
What are named capture groups?
(?<year>\d{4}) lets you read the match as match.groups.year instead of match[1], which keeps patterns readable when group order changes.