When a regex fails, check these three things first

Writing a regex takes ten minutes. Finding out why it does not match often takes an hour. This article fixes the order of investigation.

You finish a regex, and half the test cases match while the rest do not. The expensive part is never writing it — it is finding out why. The three pitfalls below cover most cases, and each has a fixed order of investigation.

Separate the two kinds of failure

No match at all and a match that captured the wrong text are different problems. The first usually means a wrong character class or flag; the second is almost always about greedy quantifiers and where the group parentheses sit. Deciding which one you have saves half the work: check whether there is a match first, then look at what group 1 actually captured.

Pitfall 1: a greedy quantifier swallows what follows

.* is greedy by default: it runs to the end of the line, then backtracks to find the rest of the pattern. So title="(.*)" applied to title="a" id="b" captures a" id="b, not a.

Two fixes: make the quantifier lazy (.*?) or use a precise character class ([^"]*). The second is usually faster and more predictable — lazy quantifiers still backtrack, character classes do not.

Pitfall 2: one escaping layer too many (or too few)

When a regex lives inside a string literal, backslashes pass through two parsers. In JavaScript you write \\d to get a regex \d; copy \\\\d from somewhere and you match a literal backslash followed by d. The same trap applies to \s, \. and \/.

Quick check: print the final pattern string in your debugging tool and compare it with what you thought you wrote. This resolves a large share of "I clearly wrote it, but nothing matches".

Pitfall 3: the m and g flags

Without m, ^ and $ match only the very start and end of the whole string, so line-by-line matching in multi-line text fails. With m, they respect every line boundary.

With g, the regex object remembers where the last match ended (lastIndex). Reusing one global regex inside a loop continues from that position on the second call, which looks like skipped matches. Create a fresh instance per iteration, or reset lastIndex to zero.

A four-step routine

  1. Shrink the input: cut the test string down to the shortest version that still reproduces the bug
  2. Bisect the pattern: split it on | or delete optional parts until you find the failing section
  3. Inspect the groups: confirm what each capture group actually contains, not what you assume
  4. Try other inputs: add one string that should match and one that should not, to rule out over-matching

Step four is the one people skip. A regex that matches your target does not prove it refuses everything else — over-matching tends to cause worse damage than a missed match, for example replacing a whole HTML fragment when you meant to touch plain text.

A note on performance

Nested quantifiers like (a+)+$ against a long non-matching input trigger catastrophic backtracking and can freeze the browser for seconds. The test is simple: double the input length and see whether the time grows more than fourfold. If it does, do not run that pattern directly on user input.

In one sentence

Decide whether it failed or captured wrongly, then check greediness, escaping and flags in that order. Following that sequence removes most of the guessing.

The three numbers to read first

When you open a regex tester, ignore the pretty highlighting for a moment and look at these three things first:

  • Total match count. Zero means "nothing matched at all" — a character-class or flag problem. More than zero but fewer than expected usually means the g flag is missing, or one part of the pattern is swallowing everything after it.
  • The start and end offset of each match. Offsets explain more than content: if the first match stretches to the end of the text, a greedy quantifier is at work.
  • What group 1 actually captured. Looking at the group in isolation instantly separates "did not match" from "matched but captured the wrong thing".

Order matters: count, then position, then groups. Do it the other way round and the coloured highlights will drag your eye around until everything looks wrong.

Three blind spots with CJK and Unicode

w is shorthand for [A-Za-z0-9_] and contains no CJK characters at all, so w+ against "张三" returns nothing. To match Chinese text use [一-龥], or the wider [一-鿿].

. does not match a newline unless the s flag is on. Against multi-line text, .* stops at the end of the first line. Either switch to [sS]* or enable s.

 word boundaries are largely useless for Chinese, because Chinese words are not separated by spaces and there is no boundary to find. Word segmentation needs a dedicated library, not a regex.

One more: Chinese punctuation is full-width. , and , are different code points, so a form that validates on a half-width comma will reject a user who types a full-width one.

Pin the pattern down with a case table

"It passed while I was testing" does not mean the regex is correct. The durable approach is three to five cases per pattern, written down as a small table:

  • Must match: a normal input, plus the boundary inputs — shortest, longest, exactly on the edge.
  • Must not match: the empty string, whitespace only, and dirty data with extra prefixes or suffixes.
  • Likely to over-match: near-miss inputs of a different class — for a phone-number check, an 11-digit-plus-prefix string or a landline starting with 1.

The table pays off later: when someone edits the pattern six months from now, running the cases shows immediately whether existing behaviour broke. Without one, every edit is a blind edit.

Where not to use a regex

Regexes are not a universal tool, and in some places they are guaranteed to fail:

  • Parsing nested HTML or XML. Tags nest arbitrarily and regexes have no recursion. <div>.*?</div> will always stop at the first closing tag and get the outer structure wrong. Use a DOM parser.
  • Parsing JSON. Strings may contain arbitrary escapes and braces, which a regex will misread. Use JSON.parse.
  • Fully validating an email address. RFC 5322 permits a staggering variety of forms, and chasing 100% accuracy is both hard to write and hard to maintain. Practically: cap the length, require one @ and at least one dot, then send a confirmation mail.
  • Very long inputs. A pattern with nested quantifiers backtracks exponentially on tens of thousands of non-matching characters and freezes the browser. Bound the input length first.

The test is simple: does the target structure allow arbitrary nesting or recursion? If yes, do not use a regex.

Related tools