Regex Tester
Highlight matches in real time, show capture groups, with g/i/m toggles
Regex tester: flags, greedy matching, and backtracking traps
What the three flags actually change
| Flag | Effect | Common misunderstanding |
|---|---|---|
g global | Find all matches, not just the first | It is stateful, see the next section |
i ignore case | Letter casing is ignored | It also expands character-class casing (so [a-z] will match uppercase too) |
m multiline | ^ and $ match the start and end of every line, not just the whole string | This is the switch that lets you "match line by line"; without it ^ only matches the very start of the text |
The third row is the most often forgotten. ^ERROR without m can only match the beginning of the first line of the whole text; when ERROR appears on line 100 it will not hit. Turn on m and each line is treated as an independent line.
There is also a frequently missed flag, s (dotAll), which makes . match newline characters too. By default . does not match newlines, so when you try to match across lines, .* silently fails — this is the number-one reason people ask "why does my cross-line match not work".
The g flag is stateful
This is a real trap that makes "the same regex work sometimes and fail sometimes". A regex object created with g keeps an internal position pointer (lastIndex):
- The first successful match leaves the pointer parked at the end of the match.
- The second match resumes from that position and does not restart from the beginning.
- If no further match is found, the pointer resets to 0; but if a match is found, the pointer keeps moving forward.
The consequence: reusing the same g regex object against several strings inside a loop drops matches. The correct fix is to create a fresh regex object for each match, or manually zero lastIndex before reusing. The decision rule is "does this pattern appear once or many times in a single string" — when you only test for existence, you should not add g at all.
Greedy versus lazy: why a match is longer than expected
Quantifiers are greedy by default: they match as much as possible. Adding ? makes them lazy: match as little as possible.
| Input | Pattern | Result |
|---|---|---|
<a>1</a><a>2</a> | <a>.*</a> | The entire string (greedy, jumps past the first </a> to the last) |
| same as above | <a>.*?</a> | <a>1</a> (lazy, stops at the first) |
To extract "the content of every tag" you must use lazy matching, otherwise you only get one big span from the first tag to the last. But lazy matching also carries a backtracking cost, covered in the next section.
Catastrophic backtracking: regex can freeze the browser
Nested quantifiers produce exponential backtracking. The classic example is (a+)+$ run against a long run of a followed by a b (such as aaaaaaaaaaaaaaaaaaaaaaaaX).
The engine tries every possible way to group the a characters, and the count grows exponentially with the length. Twenty a characters are tolerable; thirty can make the page unresponsive.
In real projects these patterns usually show up as (.*)*, ([a-z]+)*, or several .* wrapped around optional parts. Defenses:
- Remove the nested quantifier.
(a+)+anda+are functionally equivalent, but the latter has no backtracking risk. - Use a specific character class instead of
..[^"]*is far more explicit than.*and gives the engine more information to prune. - Add anchors. With a
^or a concrete prefix, failing matches get ruled out earlier. - Limit input length. Long user-submitted text should not be handed straight to a complex regex — this is an exploitable denial-of-service entry point (ReDoS).
- Test with the tool itself. This tester shows matches and capture groups; run a long input through it once and you can see whether it stalls.
Quick reference of common constructs
| Syntax | Meaning |
|---|---|
\d \w \s | Digit / word character / whitespace (uppercase forms negate) |
[abc] [^abc] | Character set / negated set |
^ $ | Line start / line end (per-line when m is on) |
\b | Word boundary, used to stop id hitting userId |
(?:...) | Non-capturing group, groups without consuming a number |
(?<name>...) | Named capture group |
(?=...) (?!...) | Lookahead: what must / must not follow |
(?<=...) (?<!...) | Lookbehind: what must / must not precede |
Two easy-to-forget rules inside character classes: a . inside [] is a literal dot (no escaping needed), and a - at the start or end is also a literal hyphen, becoming a range only when placed in the middle.
Two problem classes regex is the wrong tool for
- Parsing HTML / XML. These formats are nested structures, and regex has no recursion (it cannot express "match a balanced pair of brackets"). Except for extremely constrained cases, use a real parser — this toolbox has an XML conversion tool that uses the browser's built-in parser, which is the correct approach.
- "Fully compliant" email or URL validation. The complete grammar of an email address allows quoted local parts, comments, IP-literal domains, and more; a fully compliant regex is unmaintainably long, while the short practical regexes people actually use miss some valid addresses. The right move is a pragmatic pattern for a basic check (has an @, has a domain dot, no spaces) and leaving real verification to "send a confirmation email". Do not try to decide with a regex whether an address "exists".
This tool supports live match highlighting, shows each match and its capture groups, and offers g/i/m toggles so you can verify a pattern's behavior one case at a time. All testing runs locally in your browser.
FAQ
Why is only one match highlighted sometimes?
Without g (global) checked, the regex matches only the first result. With g it highlights all matches.
What is shown for empty capture groups?
Groups that did not participate in the match show as "undefined", which is normal and usually happens when an optional group is unmatched.