Test the failing case, not just the passing one
Most regex bugs are not "the pattern does not match" — they are "the pattern matches slightly more than intended", or "the pattern takes four minutes on one particular input". Both are invisible if you only ever test with an example that works.
The second category is worth taking seriously. Backtracking engines — JavaScript, Python's re, Java, .NET, PCRE — can take exponential time when a pattern is ambiguous, meaning there is more than one way to divide the same input between its parts. A pattern like ^(a+)+$ tested against thirty as followed by one X takes roughly a second; at forty characters it is about fifteen minutes. Our guide on catastrophic backtracking explains the mechanism and the fixes; the Safety tab applies those rules to your pattern automatically.
Nested quantifiers
(a+)+, (\d+)*, ([a-z]+)*. A quantifier immediately outside a group whose contents also repeat is exponential. It usually has a trivially equivalent safe form — (a+)+ matches exactly what a+ matches.
Ambiguous alternation
(\w|\d)* is dangerous because \d is contained within \w, so every digit can match two ways. Make alternation branches mutually exclusive, or collapse them: \w*.
Optional separators in a repeat
(\w+\s?)+$ appears in countless "validate a full name" patterns and is polynomial. Make the separator mandatory between tokens instead: ^\w+(\s\w+)*$.
Flags change the answer, not just the search
Three flags cause most of the confusion when a pattern behaves differently than expected:
g (global) — with RegExp.test() or exec(), a global regex carries mutable lastIndex state between calls, so calling test() twice on the same string can return true then false. Never reuse a global regex object across calls unless you intend that.
m (multiline) — changes ^ and $ to match at line boundaries rather than only at string boundaries. Without it, $ anchors to the very end of the input.
s (dotAll) — makes . match newlines, which it otherwise does not. This is why a pattern that works on one line silently fails across a wrapped block of text.
Capture groups are the other common source of surprise: (...) captures and is numbered left to right by opening parenthesis, (?:...) groups without capturing, and (?<name>...) captures by name. Adding a group in the middle of a pattern renumbers everything after it, which quietly breaks any code indexing matches positionally. Named groups avoid that entirely — and the Explain tab shows you every group's number at a glance.
The replacement preview
Substitutions have their own hazards. $1 inserts the first capture group, $& the whole match, and $$ a literal dollar sign — so a replacement containing a currency amount needs escaping. Previewing the result against real input is faster than reasoning about it, especially when the replacement references groups whose numbering you have just changed.
One pattern, several engines
Regular expressions are not one language. JavaScript, PCRE (PHP, grep -P), Python, Java, .NET, Go's RE2 and Rust's regex crate agree on the core — classes, quantifiers, groups, anchors — and disagree at the edges. Go and Rust guarantee linear time by refusing lookaround and backreferences entirely. Python spells named groups (?P<name>…). Ruby's ^ is always a line anchor. POSIX sed has no \d. The Code tab translates what it can and names what it cannot, which is the part that otherwise costs an afternoon.
Local, which matters for this tool
The text people test patterns against is usually the real thing: production logs, customer exports, source code. All matching here is done by the JavaScript engine in your browser tab; nothing is posted anywhere. The files you drop in the batch tab are read with the browser's FileReader and never leave your machine.