Skip to content
Control Plane Labs

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.

5 matches
GET /healthz 200 POST /v1/tokens 201 GET /v1/users?page=2 200 DELETE /v1/users/42 404 PUT /v1/users/42 500
#IndexMatch$1 <method>$2 <path>$3 <status>
10GET /healthz 200GET/healthz200
217POST /v1/tokens 201POST/v1/tokens201
337GET /v1/users?page=2 200GET/v1/users?page=2200
462DELETE /v1/users/42 404DELETE/v1/users/42404
586PUT /v1/users/42 500PUT/v1/users/42500

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.

  • 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 .md files.
  • 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?+
Because a regex with 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?+
Not necessarily. This is the ECMAScript engine, so PCRE features that other tools have are simply missing: no atomic groups (?>...), 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?+
Honestly: partially. A JavaScript 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?+
An optional group that never participated in the match returns 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.