What a regex tester is for
A regular expression is a small pattern language for describing shapes of text: three digits, then a dash, then four more; a word at the start of a line; anything between two quotes. A regex tester is the workbench where you find out whether the pattern you wrote actually describes the text you have. You paste the text, type the pattern, and see the answer immediately instead of redeploying a service to discover that your log filter matched nothing.
That feedback loop is the entire value. Regular expressions are notoriously write-only — a pattern that took twenty minutes to build is unreadable a week later, and the only reliable way to know what it does is to run it against examples. Keeping a handful of representative strings in the test box, including the ones that should not match, turns an opaque pattern into something you can reason about. Negative examples matter as much as positive ones: a pattern that matches every address you tried is only useful if it also rejects the malformed ones.
What this tool shows you
- Live matching. Results update on every keystroke in either the pattern or the text.
- Highlighting in place, so you can see exactly how much of the string each match consumed — often the fastest way to spot a quantifier that is greedier than you intended.
- A match list with the index where each match starts, its length, and the contents of every capture group, both numbered and named.
- Real error messages. An unterminated group or an invalid escape shows the engine's own
SyntaxErrortext rather than silently producing zero matches. - A replace preview, evaluating
String.prototype.replace()with$1and$<name>substitution. - A runaway guard that stops pathological patterns instead of hanging the page.
Flags, and why the same pattern behaves differently
More confusion comes from flags than from syntax. The pattern is only half of a regular expression; the flags decide how the engine applies it.
| Flag | Name | Effect |
|---|---|---|
g | global | Find every match, not just the first. Also makes replace() replace all occurrences. |
i | ignore case | A matches a. Applies to character classes and ranges too. |
m | multiline | ^ and $ anchor to each line rather than the whole string. |
s | dotAll | . also matches newline characters. |
u | unicode | Treats the pattern as code points, enabling \u{1F600} and \p{...} property escapes, and making some escapes stricter. |
y | sticky | Matches only at lastIndex — no scanning forward. Used by tokenisers. |
The g flag has a side effect that causes real bugs in application code. A global regular
expression carries mutable state: lastIndex advances after each successful
test() or exec(). Reusing one global regex object across calls therefore makes
test() alternate between true and false on the same input. If you keep a pattern in a module
constant and call test() on it, either leave g off or reset
lastIndex = 0 before each use.
Syntax quick reference
| Construct | Matches |
|---|---|
. | Any character except newline (any character at all with the s flag) |
\d \D | A digit / anything that is not a digit |
\w \W | A word character [A-Za-z0-9_] / anything else |
\s \S | Whitespace / non-whitespace |
[abc] [^abc] | Any one of a, b, c / any character except those |
[a-z0-9] | A character range inside a class |
^ $ | Start / end of the string, or of each line with m |
\b \B | A word boundary / a position that is not a word boundary |
* + ? | Zero or more, one or more, zero or one |
{2} {2,} {2,5} | Exactly 2, at least 2, between 2 and 5 |
*? +? {2,5}? | The lazy forms — match as few characters as possible |
(abc) | A capturing group, referenced as $1 and \1 |
(?:abc) | A group that does not capture — use it for grouping alternation |
(?<name>abc) | A named capture group, referenced as $<name> |
a|b | Alternation; the engine takes the leftmost alternative that succeeds |
(?=abc) (?!abc) | Positive / negative lookahead — a test that consumes nothing |
(?<=abc) (?<!abc) | Positive / negative lookbehind |
\p{L} | A Unicode property escape; requires the u flag |
Escaping is the other half of the story. Inside a pattern, the characters
. * + ? ^ $ { } ( ) | [ ] \ / are special and must be written with a backslash to match
literally. Inside a character class most of them lose their meaning, so [.+] really does mean
"a dot or a plus" — but ], \, ^ at the start, and -
in the middle still need care.
Greedy, lazy, and the classic mistake
Quantifiers are greedy by default: .* takes as much as it can and gives characters back only
when the rest of the pattern fails. Run <.*> against
<a>text</a> and you get one match spanning the whole string, because the greedy
.* ran to the end and then backed up to the last >. The lazy form
<.*?> gives you the two tags separately. Paste both into the tester above and the
highlighting makes the difference obvious in a way that reading the pattern never does.
A related trap is that a negated character class is usually better than a lazy quantifier.
"[^"]*" is both clearer and dramatically faster than ".*?", because it cannot
backtrack across a quote at all. When a pattern feels slow, replacing .*? with an explicit
"anything but the terminator" class is the first optimisation to reach for.
ReDoS: when a regex becomes a denial of service
Backtracking engines — JavaScript, Python, Java, PCRE — can be driven into exponential time by patterns
with a quantifier inside another quantifier over overlapping alternatives. The textbook example is
(a+)+$. Against a string of thirty a characters followed by an
!, the engine must try every possible way of splitting those thirty characters between the
inner and outer quantifier before it can conclude that no match exists. That is 230 attempts
for a thirty-character input, and each extra character doubles it.
This is a genuine security issue, not a curiosity. It is called ReDoS, regular-expression denial of service, and it is exploitable whenever user input reaches a vulnerable pattern — a validation regex on a signup form, a log parser, a router that matches paths. One request with a crafted string pins a CPU core, and in a single-threaded runtime like Node.js that stops the whole process from serving anyone. If an endpoint suddenly starts returning 504 Gateway Timeout under a specific input, an unbounded pattern is worth suspecting.
Practical defences: avoid nesting quantifiers such as (x+)* and (x*)*; anchor
patterns so failure is detected early; prefer negated classes to .*?; cap the length of input
you are willing to match against; and where the platform offers it, use a non-backtracking engine such as
Go's RE2 or Rust's regex crate, which guarantee linear time by refusing backreferences and lookaround.
This page has to survive the same attack, since a regex tester is precisely where people paste
pathological patterns. Note that no iteration counter can help: catastrophic backtracking happens
inside a single exec() call, before it yields anything, and JavaScript cannot
interrupt a synchronous regex — your own loop never gets another turn. So the matching here runs in a
Web Worker, on a different thread, with the page holding a timer. If the worker has not
answered within about two seconds it is terminated outright and you get an explanation instead of a frozen
tab. Where Workers are unavailable the tool falls back to matching on the main thread, and in that mode it
refuses to run patterns whose nested quantifiers make non-termination possible, because in that mode
stopping them afterwards is not an option. Either way you are told what happened and why.
Zero-length matches and other engine details
A pattern such as a* or \b can match the empty string. Iterating with a global
regex naively then loops forever, because lastIndex never advances past a zero-width match.
Every correct implementation — including this one — detects an empty match and manually increments
lastIndex. If you write your own while ((m = re.exec(s)) !== null) loop, add
that guard, or use matchAll(), which handles it for you.
Two more details that surprise people. First, capture groups inside a repeated group only keep their
last iteration: (?:(\d),)+ against 1,2,3, reports
$1 as 3, not all three digits. To collect every value you need a global match
over a smaller pattern, not one repeated group. Second, an optional group that did not participate reports
undefined, which is different from an empty string — a distinction the match list below the
tool shows explicitly, because treating one as the other is a common source of
400 Bad Request responses from over-eager validation code.
Where regex actually earns its keep in API work
Validation is the obvious use, and often the wrong one. Email addresses are the classic example: the
grammar in RFC 5322 is far more permissive than any pattern people actually write, so a strict regex
mostly succeeds at rejecting valid addresses. The pragmatic pattern in the library above checks for
something, an @, and a dotted domain — enough to catch typos — and real verification is done
by sending a message.
The places regex is genuinely the right tool are extraction and transformation. Pulling request IDs out of
log lines, rewriting a path template such as /users/:id/orders into a matcher, splitting a
Link or Content-Type value into its parts, scrubbing bearer tokens out of a
payload before pasting it into a bug report, or renaming a field across a large JSON document. For the
last of those, format the document with the
JSON formatter first — a pattern written against pretty-printed JSON
is much easier to keep correct than one fighting a single 40 KB line.
One caution worth stating plainly: do not parse structured formats with regular expressions when a real
parser exists. HTML, JSON and URLs all have recursive or context-sensitive grammars that regular
expressions cannot express. Use JSON.parse, use the URL constructor, use a DOM
parser. Regex is for the flat, line-shaped text between those structures — and for the header values that
HTTP defines as delimited lists, where a small anchored pattern is
exactly right.
Frequently asked questions
Which regex flavour does this use?
Your browser's JavaScript engine — the same ECMAScript dialect as Node.js. Compared with PCRE or
Python it lacks possessive quantifiers, atomic groups, recursion, conditionals and inline modifiers such
as (?i). Named groups, lookbehind and Unicode property escapes are all supported in current
browsers.
Why does my pattern only match once?
The g flag is off. Without it the engine returns after the first match, and
replace() substitutes only one occurrence.
Do I need to escape a forward slash?
Only inside a regex literal in source code, where / ends the pattern. The input
box here takes the pattern body alone, so https?:// works unescaped. The copy button emits a
properly escaped literal you can paste into code.
What does the tester do with a pattern that never finishes?
Matching runs in a Web Worker on a separate thread. If it has not returned within about two seconds
the worker is terminated and you get an explanation rather than a frozen tab. Two smaller guards also
apply inside the worker: collection stops at 10,000 matches, and a zero-length match advances the search
position so a* with /g cannot loop forever. Where Web Workers are unavailable,
matching happens on the main thread and patterns with nested quantifiers are refused instead of
attempted — see the ReDoS section.
Is my test data private?
Yes. Everything runs in this page; there is no upload, no logging and no storage.
Related tools and reference
API Tester
Send a real request and match a pattern against the response you get back.
JSON Formatter
Pretty-print a payload before writing a pattern against it.
Epoch Converter
Turn the Unix timestamps your pattern extracted into readable dates.
Cron Expression Generator
Build and explain the schedule strings that sit next to these patterns in config.
URL Encoder
Decode percent-encoded text so a pattern sees the real characters.
HTTP Headers
The header grammars that regular expressions are routinely used to split.