What this tool does
Matching a pattern is the easy half. The replacement string is where regex actually goes wrong, usually in one of three ways: forgetting the g flag so only the first match is replaced, writing a literal dollar sign that gets read as a capture group and swallows the character after it, or applying a broad pattern to a document and only reading the result afterwards. This tool defaults to a dry run, showing the match count and every single change before anything is written.
How it works
The replacement syntax is JavaScript's own String.prototype.replace, which means $& inserts the whole match, $1 to $9 insert numbered groups, $<name> inserts a named group, and backtick and apostrophe insert the text before and after. Anything after a dollar sign that is not one of those is treated as literal text, which is the trap: writing $5.00 as a replacement makes JavaScript look for a fifth capture group, find none, and silently delete the 5. The fix is to escape it as $$, and the tool has a button that does that escaping for the whole string, so "Total: $5.00 and $12.50" comes back as "Total: $5.00 and $12.50" rather than "Total: 5.00 and 12.50".
Flags change what gets replaced, and the g flag is the one people leave off. Without it the scan stops after the first match, which is exactly right for a pattern anchored to one line and exactly wrong for the common case. The tool shows the match count either way, so turning g off and watching the count drop from 4 to 1 is a faster way to understand it than reading about it. The other flags are all exposed with a plain-language description: m anchors ^ and $ to line boundaries, i ignores case, s lets a dot cross a newline, u enables the Unicode property escapes, and y restricts matching to the position right after the previous match.
Lookahead and lookbehind are how you find something without replacing it. A lookahead such as (?=\d) matches the position before a digit without consuming the digit, so a pattern can match an empty string at a boundary and still be useful. Zero-width matches are handled by advancing one character at a time, because a boundary pattern that returned an empty match would otherwise loop forever. A lookahead also gives you deduplication for free: matching a line and then asserting that the same line does not appear again further down collapses a run of identical lines to one without consuming the line you are comparing against.
There is a guard against catastrophic backtracking, and it is worth knowing what it does and does not do. A pattern like (a+)+ or (.*)* nests a quantifier inside a quantified group, and the backtracking engine then has exponentially many ways to fail, so a few dozen hostile characters lock the tab for minutes with no way to cancel — String.replace has no timeout. The guard refuses a quantified group that contains no fixed character, so (a+)+, ([a-z]*)* and (a*\w)+ are all rejected before compilation, and it rejects a quantified group whose alternation has overlapping branches, so (a|ab)+ is rejected too. It deliberately allows the ordinary linear shapes: (\d+) is fine, a trailing ? or a bounded {2,4} is fine, and (?:.[\w-]+)+ is fine because each pass through the group consumes a literal full stop. This is a textual heuristic, not a complete analysis, and a pattern that passes it can still be slow on a very large document, so the work happens in your browser tab and the obvious shapes are refused rather than trusted.
Worked example
Stripping HTML from a small document, then reordering an email domain, without losing the currency symbols on the way.
- Pattern <[^>]+> with an empty replacement, dry run on a three-line document: finds 6 matches and leaves the input byte-for-byte identical, so the count is visible before anything is committed
- Applying it removes the six tags and keeps the text inside them: "Order 4417 shipped on 2026-09-26."
- Pattern ([\w.]+)@([\w.]+) with $2/$1 and the g flag turns ada@example.com and grace@example.com into example.com/ada and example.com/grace, 2 matches
- Replacing prices needs the dollar escaped: pattern \$(\d+)\.(\d+) with $1.$2 gives "Total: 5.00 + 12.50" — the symbol is gone, silently, with no error
- The same pattern with $$$$1.$2 gives "Total: $5.00 + $12.50", which is the correct output
- Turning g off with pattern "a" on "a b c a b c" drops the count from 2 to 1 and leaves "X b c a b c"
Six replacements, two of which would have quietly destroyed data, all of them visible in the dry run before a single character was written.
Accuracy and limitations
- The ReDoS guard is a heuristic, not a complete analysis. It refuses the obvious nested-quantifier shapes, but a pattern that passes can still be slow on a huge document, and there is no way to cancel a running regex once it has started.
- The replacement string cannot change case. There is no upper-case token in JavaScript replacement syntax, so $1 inserts a captured character exactly as it matched; upper-casing a capture needs a second pass or a different tool.
- Very large inputs stop at 100,000 changes, because past that point a preview is no more useful than the result. The message says so rather than quietly truncating.
Frequently asked questions
- Why did my dollar sign disappear from the replacement?
- Because a dollar sign in a JavaScript replacement string is a control character, not text. $5 looks for a fifth capture group, finds nothing, and the 5 is deleted along with it. Escape it as $$, or use the escape button, which rewrites every dollar in the replacement field so it comes out literal. "$5.00" is the single most common way a regex replacement quietly loses money.
- What does $1 mean and how many can I use?
- $1 through $9 insert the numbered capture groups in the order they appear in the pattern, so (\w+)@(\w+) with $2/$1 swaps the local part and the domain. If you have more than nine groups, name them with (?<name>...) and refer to them as $<name> instead, which is more readable anyway. The tool lists the named groups it found so you do not have to count parentheses.
- How do I find something without replacing it?
- Use a lookahead, which is a zero-width assertion: (?=\d) matches the position in front of a digit without consuming the digit itself. That is how you can match a pattern, inspect what follows it, and leave the matched text alone. Lookbehind (?<=...) does the same thing looking backwards, which is useful for matching a value only when a particular label precedes it.
- Why did only the first match get replaced?
- Because the g flag was off, and without it the engine stops after one match. That is correct behaviour, not a bug: g stands for global. Turn it on when you want every occurrence replaced, and watch the match count change in the dry run, because a count that says 1 when you expected 40 is the fastest way to catch a missing flag.
- What is (a+)+ and why is it blocked?
- It is a quantified group containing a quantified group, and it is the classic catastrophic backtracking pattern. When the match fails, the engine retries every possible way of splitting the run between the inner and the outer plus, which is exponential, so a few dozen characters that fail to match can lock the browser for minutes. There is no timeout on String.replace, so the tool refuses the shape before compiling it rather than hanging.
- Is my text sent to a server?
- No. The pattern is compiled and run by your own browser through the ordinary JavaScript engine, which is exactly the same engine that will run it in your application. Nothing is uploaded, which also means the pattern behaves identically here and there, rather than being quietly reimplemented with different escaping rules on someone else's server.