Common Gotchas
Regex mistakes tend to be the same handful of mistakes, over and over, across every language and every skill level. Here's the field guide to the ones that trip up almost everyone at least once — usually exactly once, painfully, before the lesson sticks.
💡 The Bottom Line
Most regex bugs trace back to one of four habits: using a greedy quantifier when you meant a lazy one, writing a pattern vulnerable to catastrophic backtracking, forgetting to escape a character that's special to regex but meant literally, or forgetting that matching is case-sensitive by default. None of these are exotic — they're the everyday mistakes worth checking for first.
Gotcha 1: greedy vs. lazy quantifiers
By default, quantifiers like * and + are greedy —
they match as much text as possible, then back off only if needed. That's rarely what you want
when matching something like an HTML tag:
const html = "<b>bold</b> and <i>italic</i>";
html.match(/<.*>/)[0]
// "<b>bold</b> and <i>italic</i>"
// Greedy .* grabbed everything from the FIRST < to the LAST > — spanning two whole tags!
Adding a ? right after the quantifier makes it lazy — it matches
as little as possible instead:
html.match(/<.*?>/)[0]
// "<b>" — stops at the very first > it can, which is what you actually wanted
⚠️ Gotcha 2: catastrophic backtracking
Certain patterns — typically nested quantifiers like (a+)+ or
(a*)* applied to a string that almost matches but doesn't quite — can
cause the regex engine to try an exploding number of ways to backtrack and retry, freezing your
program for seconds or minutes on a short input. This is a genuinely deep topic, but the
practical takeaway is simple: be wary of quantifiers nested inside other quantifiers, especially
on patterns that process untrusted user input, and prefer simpler, more specific patterns when
you can.
Gotcha 3: forgetting to escape special characters
Characters like ., (, ), *, +,
and ? are special to regex. If you want to match them literally, you must
escape them with a backslash:
/192.168.1.1/.test("192x168y1z1") // true! — the dots matched ANY character, not literal dots
/192\.168\.1\.1/.test("192x168y1z1") // false — now the dots must be literal periods
/192\.168\.1\.1/.test("192.168.1.1") // true
Matching a literal parenthesis, like in a phone number written as (555) 123-4567,
needs the same treatment: \(555\), not (555) — the unescaped version
would try to treat "555" as a capture group instead of literal text.
Gotcha 4: case sensitivity
Regex matching is case-sensitive by default:
/hello/.test("Hello world") // false — capital "H" doesn't match lowercase "h"
/hello/i.test("Hello world") // true — the /i flag makes matching case-insensitive
The i flag (short for "insensitive") is added right after the closing slash of a
JavaScript regex literal, and is available in essentially every regex engine in one form or
another.
🙋 There Are No Dumb Questions
Q: How do I know if my pattern is at risk of catastrophic backtracking?
A: The clearest warning sign is a quantifier wrapped around a group that itself contains a
quantifier — things like (a+)+, (a|a)*, or (.*)*. If
you're not sure, test your pattern against a long, almost-but-not-quite-matching string (like
50 "a"s followed by a "b" when your pattern expects all "a"s) and see if it hangs. Many online
regex testers will also flag this risk for you automatically.
🔧 Try It Yourself
Take the pattern /<.*>/ and the string
"<p>one</p><p>two</p>". Predict what it matches before
running it, then fix it with a lazy quantifier so it matches just the first
<p> tag.
✅ Quick Check
Why does /<.*>/ often match more text than intended across multiple HTML
tags?
That's the whole track — time for the recap.