Regex not matching? A debugging checklist
When a regex that looks right doesn't match, the cause is usually one of a dozen things: hidden characters, lost backslashes, an unescaped dot, a missing anchor or flag. Here they are in the order to check them, with examples you can try.

· 6 min read
/1\.5/
match
- 1.5
- v1.5.2
spare
- 125
- 1x5
When a regex looks right but doesn't match, the pattern is rarely wrong in an interesting way. It's usually one of a dozen ordinary causes: the input has a character you can't see, a backslash was lost in a string, a special character isn't escaped, an anchor or flag is missing, or the code runs the regex differently from your tester. Below is a checklist in the order worth checking, with live examples.
The examples run JavaScript's engine; where Python or another language behaves differently, the item says so.
01First, a 60-second method
- Print the input exactly. In Python,
print(repr(text))shows hidden characters as escapes like\xa0(a non-breaking space),\u200b(a zero-width space) or\r. In JavaScript,[...text].map(c => c.codePointAt(0).toString(16))lists every character's code, soa0or200bstands out. - Shrink the pattern. Delete pieces from the end until it matches, then add them back one by one. The piece that breaks it is the bug.
- Write two lists of test lines: a few that must match and a few that must not. Paste both into the Playground, whose Anatomy panel explains each part of the pattern in plain English.
02The input isn't what you think
Text copied from web pages, PDFs and chat apps often holds characters that look like ordinary ones but aren't: a non-breaking space instead of a space, a zero-width space inside a word, curly quotes instead of straight ones. The two lines below look identical. The second has a non-breaking space after the colon, so a pattern with a plain space misses it:
\s matches both kinds of space in JavaScript, so it's the fix when the source can't be trusted:
A zero-width space is worse: hello prints as "hello" but has six characters, and ^hello$ won't match it. Curly quotes “hi” won't match "hi". Printing the input (step 1 above) shows all of these.
03The backslashes didn't survive
A pattern written inside a normal string loses its backslashes before the regex engine sees it:
- JavaScript:
new RegExp("\d+")becomes the patternd+, which matchesdddand not42. Writenew RegExp("\\d+"), or use a literal:/\d+/. - Python: always use raw strings,
r"\bcat\b". Without ther,"\b"is a backspace character, so"\bcat\b"never matchesa catand gives no error. - Java: every backslash is doubled in source code:
"\\d+","\\.".
If a pattern works in a tester but not in code, this is the first suspect.
04A special character isn't escaped
. * + ? ^ $ | ( ) [ ] { } \ all mean something. An unescaped dot is the classic case, because the pattern still "works", just too well: 1.5 matches 125.
The same goes for $ in prices, ? in URLs and + in phone numbers: put a backslash in front, or put the character in a class like [.] or [+].
05It matches part of the string, or none of it
A regex search finds a match anywhere in the text. \d{3} "validates" 12345abc because three digits appear somewhere in it. For validation, anchor both ends: ^\d{3}$.
Python adds a twist: re.match only anchors the start, so re.match(r"\d{3}", "12345") succeeds. Use re.fullmatch for "the whole string".
The opposite problem: ^ and $ mean the start and end of the whole text unless you set the m flag, so ^ERROR finds nothing on line 5 of a log until you add m. And files with Windows line endings (\r\n) trip Python: with re.M, its $ doesn't match before \r, so ^abc$ fails on abc\r\n. JavaScript's $ does match there. Strip the \r first, or allow it: ^abc\r?$.
More in Anchors and the m flag.
06A flag is missing
i: without it,hellodoesn't matchHello.s: without it,.doesn't match a newline, soa.bmissesaandbon two lines.g: without it, JavaScript'sreplacechanges only the first match:"a-b-c".replace(/-/, "+")givesa+b-c.u: without it, an emoji counts as two characters, so^.$doesn't match😀.
07The g flag remembers where it stopped (JavaScript)
A regex with the g flag keeps lastIndex between calls. Reuse it with test() and the results alternate:
const re = /cat/g;
re.test("cat"); // true
re.test("cat"); // false: the search started at index 3
re.test("cat"); // true again
Drop the g for test(), or create the regex inside the function. The same applies to exec() in a loop that never reaches the end.
08It matched too much
.* is greedy: in <b>x</b><b>y</b>, the pattern <b>.*</b> takes everything from the first <b> to the last </b>. Use .*?, or better, a class that can't run past the end, like [^<]*. Greedy vs lazy quantifiers walks through it.
09The character class doesn't mean what it says
[a-Z]is an error ("range out of order"): uppercase letters come before lowercase. Write[a-zA-Z].- A hyphen in the middle makes a range.
[\w-.]happens to work without theuflag and is an error with it. Put the hyphen first or last:[\w.-]. ^only negates at the very start.[^abc]is "not a, b or c";[a^bc]is "a, ^, b or c".
10Your tester runs a different engine
A pattern built in a PCRE tester can fail in JavaScript, Python or Go: named groups are written differently, lookbehind limits differ, and Go's engine has no lookarounds at all. Regex engines compared has the table. Test in the engine your code runs; the Playground runs JavaScript's.
11It doesn't fail, it hangs
If the regex takes seconds on some inputs, it isn't a mismatch problem: nested quantifiers like (a+)+ make a backtracking engine try an enormous number of paths. Catastrophic backtracking shows how to spot and fix it.
12Keep it from coming back
Save the test lines next to the pattern: a comment with two or three lines that must match and two or three that must not, or a unit test. The Playground can share the pattern and your test lines in one link, which is handy in a code review. And if you'd like the debugging reflexes to become automatic, the daily hunt is five minutes of exactly this: lines to catch, lines to spare, and a pattern to fix until both are right.