Why does my regex match nothing at all?
g flag when you expected repeated matches, an unescaped special character, or anchors (^/$) combined with an "off" m flag when your text has multiple lines.Regex debugging is hard mostly because failures are silent — a pattern that matches nothing doesn't throw an error, it just quietly returns zero results. The fastest way to isolate the problem is to paste both the pattern and a small, realistic sample string into a tester that highlights matches as you type, then remove pieces of the pattern one at a time until something lights up.
A common trap: forgetting that ., (, ), +, and ? are special characters. If you want a literal period (as in a filename or an IP-style string), it needs to be escaped as \. — otherwise it silently matches "any character," which can look like it's working until an edge case breaks it.
^ and $ anchor to the start/end of the whole string by default. Checking the m (multiline) flag makes them anchor to the start/end of every line instead — a frequent source of "it matches line 1 but not line 3" confusion.Why does my regex match too much (it's too greedy)?
*, +, {n,m}) are greedy by default, grabbing the longest possible match. Add a ? right after the quantifier to make it lazy instead, so it grabs the shortest possible match.Take the pattern <.*> against the text <b>bold</b>. A greedy quantifier stretches from the very first < all the way to the very last >, swallowing the whole string instead of just the opening tag. Changing it to <.*?> makes the quantifier lazy, so it stops at the very next > it finds.
Here's an example of how the same input behaves differently depending on greediness — this is illustrative, not a fixed rule for every pattern:
| Pattern | Behavior | Match on this example |
|---|---|---|
| <.*> | Greedy | <b>bold</b> |
| <.*?> | Lazy | <b> |
Watching the live highlight change as you toggle a single ? is usually faster than reasoning through it in your head, especially with nested tags or repeated delimiters.
How do I read capture groups correctly?
(\d{2})-(\d{2}) appear in order, and named groups written as (?<name>...) appear by name — both listed alongside the match position for each hit.It's easy to miscount groups once a pattern has several optional or nested parentheses. A non-capturing group, written (?:...), groups characters together without being numbered — so if your pattern mixes capturing and non-capturing groups, the numbering can shift in ways that aren't obvious just from reading the pattern. Confirming against a real sample string, and checking exactly which value landed in group 1 versus group 2, avoids off-by-one mistakes before they reach production code.
How do I preview find & replace safely?
$1, $2… (or $<name> for named groups) and preview the full output before running the substitution anywhere that matters.For example (illustrative only), running the pattern (\d{4})-(\d{2})-(\d{2}) with replacement $2/$3/$1 against the sample string "2026-07-19" produces "07/19/2026." Previewing this kind of transformation against a handful of representative lines first catches a mistyped group reference — like $2 where you meant $3 — long before it silently corrupts real data.
If your replacement should apply to every match rather than just the first one, make sure the g flag is turned on; without it, only the first occurrence in the string is replaced.
What about catastrophic slowdowns (ReDoS)?
A less common but nastier failure mode is a pattern that works fine on short test strings but appears to hang on longer or more repetitive input. This is typically caused by ambiguous nested quantifiers — patterns like (a+)+ — where the matching engine can try an exponential number of ways to fail. A quick ReDoS check runs a pattern against a few adversarial repeated-character strings and flags anything that takes noticeably longer than expected, which is a useful early warning before that pattern ever reaches a server that processes untrusted input.