You write a regex. It works on your test string. It ships. Then a real user types something slightly different and the whole validation falls over, or worse, it silently accepts input it should have rejected.
Most regex bugs are not syntax errors. The engine will happily tell you about an unbalanced bracket. The dangerous bugs are the ones that compile fine and match the wrong thing: a dot that was meant to be literal, a missing flag, a capture group in the wrong position.
The dot that matches everything
The classic one: validating a file extension with \.pdf versus .pdf. The first matches only a literal dot followed by pdf. The second matches any character followed by pdf, so xpdf passes too. One backslash, totally different behavior, and both versions compile without complaint.
Same family: ^ and $ without the multiline flag. On single-line input they behave exactly as you expect. On multi-line input they anchor to the start and end of the whole string, not each line, and your per-line validation quietly stops working.
The flag you forgot
g is the flag people forget most. Without it, String.replace replaces only the first match, and matchAll throws outright. So a find-and-replace that works perfectly on a one-match test string ships broken on real text with five matches, and nobody notices until a user reports that only the first occurrence changed.
i is the quiet one. A pattern tested against lowercase emails works until someone types Ada@Example.COM, and suddenly a validation that passed in development rejects a real user in production.
Groups are positions, not names
In (\w+)@(\w+), group 1 is the username and group 2 is the domain, because groups are numbered by the order their opening parentheses appear. Add one more group at the front later, for an optional title or prefix, and every group number after it shifts. Code that read group 2 for the domain now reads something else, and nothing throws.
Named groups fix this: (?<user>\w+)@(?<domain>\w+) keeps working no matter how many groups you add around it. If a pattern has more than two groups, name them. Future you will be grateful.
Test against hostile input, not friendly input
The fix for all of this is boring and effective: run the pattern against real text before it ships. Not the one clean example from the ticket, but the messy version: uppercase, extra spaces, missing domains, multi-line input, empty strings, and the longest realistic input you have. That last one matters for a different reason: nested quantifiers like (a+)+ on near-matching input can take exponential time, a flaw called catastrophic backtracking, and it only shows up on long inputs.
I keep a small tester open for exactly this: paste the pattern, toggle the flags, paste the messy text, and read the match table. It shows every match with its index and every capture group, named or numbered, so a shifted group or a missed flag is visible before the code ships. It also previews find-and-replace with your exact flags, so the missing-g bug shows up as "only the first one changed" right there in the preview instead of in production.
If you write regex for validation or extraction, test it the way users will break it: Regex Tester.
It runs entirely in your browser, so pasting real log lines or customer text into it sends nothing anywhere, and the pattern that passes there behaves identically in your JavaScript code, because it uses the same engine.
Top comments (0)