Regex Tester
Test JavaScript regular expressions with live match highlighting, a capture group table, replace preview, and ReDoS guardrails.
Privacy: Matching runs in your browser with its built-in RegExp engine. Neither the pattern nor the test string is uploaded or logged.
| # | Index | Match | $1 <method> | $2 <path> | $3 <status> |
|---|---|---|---|---|---|
| 1 | 0 | GET /healthz 200 | GET | /healthz | 200 |
| 2 | 17 | POST /v1/tokens 201 | POST | /v1/tokens | 201 |
| 3 | 37 | GET /v1/users?page=2 200 | GET | /v1/users?page=2 | 200 |
| 4 | 62 | DELETE /v1/users/42 404 | DELETE | /v1/users/42 | 404 |
| 5 | 86 | PUT /v1/users/42 500 | PUT | /v1/users/42 | 500 |
Matching uses your browser's own RegExp engine and runs entirely in this tab — no pattern or test string is uploaded. Safety limits: input capped at 20,000 characters, 2,000 matches, and a 250 ms budget across the match loop. Pattern, flags, and replacement persist in the URL; the test string is dropped from the link once the whole thing exceeds 2 KB.
What it does
Edit a pattern and a test string side by side and see every match highlighted as
you type. Below the panes, a table lists each match with its index, the full
match, and every capture group — numbered and named, with undefined printed
literally so an optional group that never participated is distinguishable from
one that matched an empty string. A replace view previews substitutions using
the real $1, $<name>, $&, and $$ syntax, and a cheatsheet tab holds the
syntax reference plus a handful of loadable starting patterns.
How it works
The engine is your browser’s own, so what you see is
ECMAScript RegExp
behavior as specified in
ECMA-262 —
not an approximation of it. All six flags are exposed: g, i, m, s, u,
y. If g is off, exactly one match is reported, because that is what
exec() does; the tool tells you to add the flag rather than quietly doing it
for you.
Matching iterates with exec() and resets lastIndex first, which sidesteps the
most common real-world bug with global regexes: a shared regex object carries its
lastIndex between calls, so a module-level const re = /x/g used with
test() appears to alternate between true and false on identical input.
Substitution is implemented directly rather than delegated to replace(), so the
preview can be built from the same capped match list shown in the table. It
follows the replacement patterns documented for
String.prototype.replace.
Why the tool refuses to run some patterns
Backtracking engines can take exponential time on patterns where a quantifier
wraps something that is already quantified. The canonical example is (a+)+b:
against a string of 30 as with no b, the engine explores every partition of
the run before concluding it cannot match, which takes about a minute of solid
CPU. That is
regular expression denial of service,
and it has taken down production services that let users supply patterns — or
merely supply input to a badly written one.
The important and often-glossed-over detail is that a single exec() call cannot
be interrupted. JavaScript regex matching is synchronous and has no yield point,
so an “iteration cap” is not, by itself, protection. What is here instead is
layered and deliberately conservative: the pattern is screened for the two
classic exponential shapes and is not executed until you tick “run it anyway”,
the test string is capped at 20,000 characters, the match loop stops at 2,000
matches, and the loop abandons after a 250 ms budget. The screen is a heuristic.
It will produce false positives on patterns that are fine in practice, and it
will miss shapes it does not know about.
If you take one thing from this: server-side, a user-supplied pattern is
user-supplied code. Run it in something you can kill, or use an engine with
linear-time guarantees. Go’s regexp package uses RE2 and refuses
backreferences and lookaround precisely so it can promise that.
When to reach for it
Working out why a log-parsing pattern misses the last line — usually a missing
m flag, or $ anchoring to the end of input rather than the end of a line.
Checking what a capture group actually contains before you index into it in
code. Building a search-and-replace before running it across a repo, where
getting $1 wrong the first time means a dirty diff. Confirming that \d is
still just [0-9] and that the Unicode digits in that pasted CSV need \p{Nd}
and the u flag. And demonstrating to someone that a pattern is dangerous
without taking their browser down to prove it.
Related tools
- JSON formatter for the payload you are about to scrape with a pattern (parse it properly instead, where you can).
- URL encoder/decoder for query strings that need decoding before a pattern will match them.
- Markdown preview when the bulk edit is over a directory of
.mdfiles. - Hash generator for checking a match against a known digest.
- Timestamp converter for the ISO 8601 strings your pattern just pulled out of a log line.
Frequently asked questions
Why does my <code>/g</code> pattern skip every other match in my own code?+
g or y carries mutable state: lastIndex. Calling test() or exec() twice on the same regex object resumes from where the previous call stopped, so a shared module-level const re = /x/g appears to alternate between true and false. Either create the regex where you use it, reset re.lastIndex = 0, or use String.prototype.matchAll. This tool always resets lastIndex before iterating.Will a pattern that works here work in Python, Go, or grep?+
(?>...), no possessive quantifiers a++, no recursion, no \A/\z/\Z anchors, no inline flag groups (?i), and no POSIX classes like [[:digit:]]. Go's RE2 goes the other way and refuses backreferences and lookaround entirely, so a pattern that runs here can be rejected outright by Go or by anything that embeds RE2.How does the ReDoS protection actually work?+
exec() call cannot be interrupted once it starts, so no iteration cap can stop a single catastrophic match — (a+)+b against 30 as takes over a minute in a real engine and freezes the thread it runs on. So the defense is layered: the pattern is screened for the classic exponential shapes (a quantifier wrapping a group that already has one, alternation inside a quantified group) and is not run until you explicitly opt in, the test string is capped at 20,000 characters, the match loop stops at 2,000 matches, and the loop abandons after a 250 ms budget. The screen is a heuristic and will miss cases. On a server, treat untrusted patterns as untrusted code and run them somewhere you can kill.Why is one of my capture groups <code>undefined</code> instead of an empty string?+
undefined; a group that matched zero characters returns "". The two are different and the distinction is load-bearing when you branch on it — if (m[2]) treats both the same way, which is usually a bug. Alternations are the common source: in (a)|(b) exactly one group is ever defined. The table above prints undefined literally so you can tell which case you are in.Why doesn't <code>\d</code> match non-ASCII digits, and when do I need the <code>u</code> flag?+
\d is defined as [0-9] and nothing else, and \w is [A-Za-z0-9_] — neither is Unicode-aware, in any flavor. For real Unicode classes you need the u flag and property escapes: \p{Nd} for any decimal digit, \p{L} for any letter, \p{Script=Greek} for a script. The u flag also makes . and quantifiers operate on whole code points rather than UTF-16 units, which is what you want any time emoji or astral characters can appear.Related tools
JSON Formatter and Validator
Paste JSON, get it pretty-printed or minified. Validates syntax, highlights errors, works entirely in your browser.
URL Encoder/Decoder
Percent-encode and decode URLs with the right rules: component, full-URI, or form encoding, plus a query string parser and builder.
Markdown ↔ HTML Preview
Side-by-side Markdown editor with GitHub-flavored Markdown support, live preview, and a sanitized HTML pane you can copy.