Every developer tests a regular expression the same way: paste in one string that should match, one that shouldn’t, watch both go green, ship it. That workflow is fine right up until the afternoon a regex that passed every test you wrote pins a CPU at 100% and stops answering requests. The pattern wasn’t wrong. It was slow in a way no amount of “does it match?” testing will ever reveal.
So here’s how to actually test a regular expression — the free ways, what to check beyond matching, and the one failure mode that turns a harmless-looking pattern into an outage.
The free way to test one
You don’t need anything installed. Open your browser console and you have a full regex engine:
/(\w+)@(\w+)\.(\w+)/g.exec("contact a@b.com")
That returns the full match plus each capture group. For a quick pass over every match instead of just the first, [...str.matchAll(re)] gives you the lot, each with its index. In Python it’s re.findall and re.finditer; in most languages the REPL is thirty seconds away. Interactive sites that highlight matches as you type are genuinely useful for building a pattern up, and you should use them for that. Just know what they don’t tell you, which is most of this article.
What to actually test for
“It matched my example” is the weakest possible assertion. The cases that break patterns in production are the ones nobody pastes in:
- The near-miss. Not just a string that matches, but one that almost does. Most regex bugs are over-matching, and you only see them with input that should have been rejected.
- Empty and whitespace. An empty string, a string of spaces, a trailing newline. Patterns that look airtight often happily match nothing at all.
- Anchors. Without
^and$, your “valid ID” pattern will cheerfully match a valid ID buried inside a paragraph of garbage. - Greedy vs. lazy.
<.*>on<a><b>swallows the whole line;<.*?>takes one tag. Nearly every “why did it grab too much?” bug is this. - Multiline and Unicode.
.doesn’t cross newlines without the right flag, and\wmay not mean what you assume once real names and accents show up.
The failure mode nobody tests: catastrophic backtracking
This is the one that matters, and it’s invisible to normal testing. Most regex engines backtrack: when a match fails, they back up and try another combination. Certain pattern shapes make the number of combinations grow exponentially with input length. The classic is nested quantifiers — something like (a+)+$. Hand it twenty a characters followed by a ! and the engine has to explore an astronomical number of ways to split those characters before it can conclude there’s no match.
On your test string of ten characters, it returns instantly. On a forty-character input from a user, it runs for longer than the heat death of your request timeout. Because regex matching is synchronous and CPU-bound, that single request pins a core and stops serving anyone else. If the pattern touches user input — a search box, a validation field, a log parser — you have just shipped a denial-of-service vulnerability with a one-line diff. It has a name: ReDoS.
The shapes to be suspicious of
A quantifier applied to a group that itself contains a quantifier — (a+)+, (a*)*, (\d+)*. Alternation where the branches can match the same text, like (a|a)+. And any of those followed by something that can fail, which is what forces the engine to explore every combination before giving up. If your pattern has nested quantifiers and runs against untrusted input, assume it’s dangerous until proven otherwise.
The fix is usually simpler than the diagnosis: make the inner quantifier unnecessary (a+ instead of (a+)+), anchor the pattern so failure is detected early, replace a greedy wildcard with a specific character class, or use an engine that doesn’t backtrack at all — RE2 and its ports guarantee linear time by refusing to support the features that cause the blowup.
This is also why our $1 regex tester refuses to run a pattern it detects as catastrophic instead of hanging on it — it returns matches, capture groups, named groups and positions for safe patterns, and tells you plainly when a pattern is the dangerous shape. The browser-console method above is free and works fine; the difference is that the console will happily execute the pattern that takes your server down, and tell you nothing.
When the answer isn’t a regex
The last test is whether you should be using one at all. Regex matches patterns, not structures — so parsing HTML, JSON, or nested anything with it is a well-known road to misery, because those formats can nest arbitrarily and a regular expression fundamentally cannot count. Use a parser. The same goes for email validation: the genuinely correct email regex is thousands of characters long and still wrong, so check for an @, then send a confirmation link, which is the only validation that ever actually proved anything.
The short version
- Test the near-miss, not just the match.
- Anchor it, or it’ll match inside garbage.
- Check greedy vs. lazy the moment it grabs too much.
- Hunt nested quantifiers before untrusted input finds them for you.
- If it needs to count or nest, use a parser instead.
A regex that matches your example is not a tested regex. It’s a regex that hasn’t met production yet.
We build software that assumes user input is hostile, because it eventually is. That’s what we do at Rebel Studios.