Most regex afternoons are lost to one of three mistakes. A quantifier is greedy and takes more than you meant. A dot refuses to cross a line break. Or a replacement only touches the first match because the g flag is off. None of them throws an error. Each returns a result that is plausible on the one example you tried, and that is what makes them expensive: you do not know anything is wrong until the pattern meets real data.
Why does my pattern match far more than I wanted?
Because * and + are greedy. They take as many characters as they can and give them back one at a time only until the rest of the pattern can match. Run <.*> over <b>hi</b> and <i>yo</i> and you get one match: the whole string, from the first < to the last >. The pattern did what you wrote. You wrote "any run of characters between a bracket and a bracket", and the longest such run is everything.
There are two fixes and they are not the same. A question mark after the quantifier makes it lazy: <.*?> stops at the first > and returns four matches (<b>, </b>, <i>, </i>). A negated class says what you actually mean: <[^>]*> is "a bracket, then anything that is not a closing bracket, then a closing bracket". It gives the same four matches on this text. It is also the safer habit, because a lazy dot still crosses any character it needs to, and a negated class cannot.
The giveaway is a match count lower than you expected, with one match that is suspiciously long. Paste your text into the regex tester and look at the highlight: a single block of colour running across several of the things you wanted separately is greedy matching, every time.
Why does the pattern stop at the end of a line?
Because the dot matches any character except a line break. Take three lines, Name: Ada, Role: Admin, Name: Grace, and the pattern Name:.*Admin. It finds nothing. The text is right there, but the dot cannot step over the newline between the first line and the second.
This one costs the most time because the pattern worked when you tested it on a single line. You pasted one line into the box, got a match, and shipped the pattern to code that reads a whole file. The usual cause of "it works in the tester and returns nothing in production" is this, or the flag below.
Two ways out. The s flag lets the dot match a newline; the regex tester has a checkbox for it. Or write [\s\S], which means "whitespace or not whitespace" and therefore any character. The find and replace tool has no s option, so [\s\S] is what works there.
Fixing it can cause the first mistake. Turn on s and a greedy Name: (.*) no longer stops at the end of its line: it returns one match that runs from the first Name: to the end of the text, taking the other two lines with it. With the dot confined to a line, greediness was harmless. With the dot unleashed, it is not, so lazy or negated classes become necessary at the same moment.
A related trap is ^ and $. Without the m flag they mean the start and end of the whole text, so ^Role matches nothing in the middle of a file. The two tools here differ: the tester starts with m off, while the find and replace tool starts with "^ and $ match every line" on. A pattern copied from one into the other can change meaning without any edit.
Why was only the first match replaced?
Because the g flag is off. Without it the engine stops after the first match, and a replacement stops with it. In JavaScript, replacing - with + in a-b-c gives a+b-c without g and a+b+c with it.
The trap is that nothing looks broken. On a one-line test the result is right. On a document of two thousand lines the first hit is fixed and the other 1,999 are left alone, and unless you search for the old text afterwards you will not see it. When the replacement is wrong by exactly "all of them except one", look at the flags first.
The same flag has a second effect in code. A regex object created with g remembers where its last match ended, in a property called lastIndex. Calling .test() on it twice with the same string returns true, then false, because the second search starts after the first match. If a check passes on some inputs and fails on identical ones in a loop, a shared /g regex is a likely reason. Drop the flag when you only need a yes or no. The tester on this site builds a fresh pattern on every keystroke, so it cannot reproduce this; it only shows up in code that reuses one.
How do I tell which mistake I am looking at?
Count before you read. Work out how many matches you expect, then compare it with the number the tool reports. The mismatch points at the cause:
- Fewer matches than expected, one of them very long: greedy quantifier.
- Zero or fewer matches, on text that spans several lines: the dot, or
^and$withoutm. - The right number of matches, but the replacement only changed one:
gis off. - The count is right and the text is clearly there, but nothing matches: something in the text is not what it looks like. A non-breaking space or a curly quote will defeat any pattern, and the invisible characters in text are worth ruling out before you rewrite the regex.
Then shrink the problem. Cut the text to the smallest sample that still goes wrong, and cut the pattern to the part that is wrong, adding pieces back one at a time. A regex that fails as a whole usually has one piece that matched something you did not expect, and you only see which piece by building up from nothing. Reading a long pattern for the error is slower than rebuilding it.
When a replacement is involved, check the output as well as the matches. Find and replace mistakes covers how to confirm what actually changed, because a correct match list can still produce a wrong result if the replacement string contains a $.
What will the tester not catch?
A few limits, so you do not trust a green result further than it goes.
- It runs your browser's JavaScript engine and nothing else. Lookbehind, named groups and property escapes differ between JavaScript, Python, PCRE and Go, so a pattern that passes here can fail in another language. Test in the language you will ship in.
- It tests the first 50,000 characters of the text you paste and lists the first 50 matches. A problem that only appears on page 80 of a long log will not show up.
- It cannot stop a catastrophic pattern. Nested quantifiers such as
(a+)+$on the wrong input can freeze the tab, and nothing can interrupt a regular expression once it has started. The 50,000-character cap keeps most bad patterns finishing, and it is a mitigation, not a guarantee. - It shows you what your pattern does, not whether it is the right tool. Parsing HTML with a regex fails on the next file even when it works on this one; HTML entities are one reason the text does not look like what you matched. The browser's built-in
DOMParseris the better route.
The pattern, the text and the replacement are all handled by your browser. Nothing is uploaded, which matters here because people test patterns on log extracts and customer records.
To see the three mistakes for yourself, paste the examples above into the regex tester and toggle the g, s and m checkboxes. The highlight and the match count change as you click, which teaches the difference faster than a description does.
If the pattern is right and the result is still wrong, the text probably contains characters that are not the ones you see. Invisible characters in text explains how to find them.
Frequently asked questions
Why does my regex match more text than I expected?
Quantifiers such as * and + are greedy by default: they take as many characters as they can and give them back only until the rest of the pattern matches. Adding a question mark makes the quantifier lazy, so <.*?> stops at the first closing bracket. A negated class such as <[^>]*> is usually safer because it cannot cross the character you are looking for.
Why does the dot in my regex not match a newline?
In JavaScript the dot matches any character except a line break, so a pattern that works on one line can return nothing on a paragraph. Turn on the s flag to let the dot cross lines, or write [\s\S] instead of the dot. The m flag is a separate setting: it changes what ^ and $ mean and leaves the dot alone.
Why does my regex replace only the first match?
Because the g flag is off. Without it, the engine stops at the first match and the replacement stops with it, so in a long text one occurrence is changed and the rest are left alone. Turn the g flag on to replace every occurrence.
Why does my regex return true and then false on the same string?
A regex object with the g flag stores where its last match ended in lastIndex, and the next call starts from there. Calling test() twice on the same string therefore returns true and then false. Remove the g flag when you only need a yes or no answer, or create a new regex for each check.
Why does my pattern work in the tester but not in my code?
The usual causes are a different regex engine, a missing flag, or text that differs from what you pasted. Lookbehind, named groups and property escapes vary between JavaScript, Python, PCRE and Go. Real data also tends to contain line breaks, non-breaking spaces and curly quotes that a one-line test did not.
Last updated October 19, 2026