Regex Tester

Live pattern matcher with flags and groups

A pattern box, a flags box, and a block of test text. Every keystroke recompiles the pattern with new RegExp(pattern, flags) and re-runs it, so a mistake surfaces as the engine's own message — Invalid regular expression: /(a/: Unterminated group — rather than a blank result you have to guess at.

Matches are highlighted in place and also listed individually, each with the character offset it started at and the contents of every numbered capture group. The offset is the field that tells you a pattern is matching in the wrong place, which looks identical to matching correctly until you check.

This is the ECMAScript engine, running in your tab. That is simultaneously the most useful thing about it and the most important thing to remember before you paste the pattern into a Go service or a Python validator — see the engine notes below.

Reading the match list

  • Total Matches — always every match in the text. The tester adds the g flag internally for enumeration even if you left it out of the flags box, so this count is not what a single .exec() would give you in your own code.
  • Preview — your text with each match wrapped in a highlight, so overlapping expectations and unexpected whitespace show up visually.
  • Match rows — #3 @ 47 means the third match begins at index 47. That index counts UTF-16 code units, so an emoji earlier in the string advances it by two, not one.
  • Groups — listed as #1=, #2= in order. An optional group that did not participate shows as empty rather than as undefined.

Zero-length matches are handled rather than hung on. A pattern such as ^, \b or a* can match the empty string, and a naive enumeration loop never advances past one. The tester nudges the cursor forward by one code unit after an empty match, which is why a* against bbb reports matches instead of freezing.

Flags, and the two that change what a character is

The flags box takes the standard ECMAScript set: g global, i case-insensitive, m multiline (so ^ and $ bind to line breaks rather than to the whole string), s dotAll (so . matches a newline), y sticky, d indices, plus u and v.

u is the one that changes results rather than scope. Without it, the pattern . matches a single UTF-16 code unit, so running /./g over one emoji reports two matches and the highlight visibly cuts the character in half. With u, the same pattern reports one. Any pattern that will meet user-supplied text — names, messages, filenames — wants u on it. It also unlocks \p{...} property escapes, so \p{Letter} becomes available instead of the ASCII-shaped [A-Za-z].

v is the ES2024 successor: it adds set difference and intersection inside character classes, as in [\p{Letter}--[a-z]], and lets a class contain multi-character strings. u and v are mutually exclusive — type both into the flags box and the constructor throws, which the error row will show you verbatim.

Catastrophic backtracking, and the engines this tester is not

ECMAScript's RegExp is a backtracking engine, and backtracking engines have a failure mode that is not slowness but a wall. Take (a+)+$ against a run of a's ending in a single b. The inner a+ and the outer + can carve that run into exponentially many partitions, and the engine tries all of them before concluding there is no match. At twenty characters it is imperceptible; around thirty it becomes seconds; a little beyond that it is effectively forever. Because the tester runs synchronously on the page's main thread, the tab freezes and you reload it. Nothing has been sent anywhere — the browser is simply busy.

OWASP catalogues this as ReDoS and lists the classic shapes: (a+)+$, ([a-zA-Z]+)*$, (a|aa)+$ and (a|a?)+$. The common ingredient is a quantifier wrapped around something already quantified or ambiguous, followed by a condition that can fail. If your pattern has a + or * immediately outside a group that also ends in + or *, test it against a long non-matching string here before it goes anywhere near a request handler.

Two families of engine cannot fail this way. RE2, which backs Go's regexp package, and Rust's regex crate both run in time linear in the input length. The price is that they refuse the features that make backtracking necessary in the first place: RE2's syntax reference marks backreferences (\1) and every form of lookahead and lookbehind as not supported. A pattern using (?=...) that works perfectly in this tester will simply fail to compile in Go. .NET offers the same trade optionally through RegexOptions.NonBacktracking.

Character classes differ too, and more quietly. In JavaScript \d is exactly [0-9]. In Python 3, \d against a str pattern matches Unicode decimal digits by default, so Arabic-Indic digits such as ٣ satisfy it — meaning a validator you prototyped here can accept, in production, digits your backend cannot parse. \w and \s diverge on the same axis. Why a Regex That Works in Testing Can Fail (or Hang) in Production goes through the rest.

Three patterns and what the panel shows

  1. The default. Pattern \b\w+\b, flags g, text hello world. Two matches: #1 @ 0 and #2 @ 6. The offsets confirm the space was not consumed.
  2. Capture groups. Pattern (\d{4})-(\d{2})-(\d{2}) against shipped 2026-09-09, due 2026-12-25. Two matches, each with three groups: the first reads #1=2026, #2=09, #3=09 and the second #1=2026, #2=12, #3=25. This is the fastest way to check that a group is capturing the field you think it is rather than the one next to it.
  3. Greedy versus lazy. Against the text <b>bold</b>, the pattern <.+> gives one match spanning the entire string, because .+ takes everything it can and then gives back only enough to find a final >. Add a single character — <.+?> — and you get two matches, <b> at offset 0 and </b> at offset 7. This one difference is behind a large share of "my scraper grabbed the whole line" bugs.

Patterns worth pasting in, and what each gets wrong

PurposePatternWhat it will get wrong
Email, pragmatic^[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}$Rejects quoted local parts and non-ASCII addresses; accepts domains that do not exist.
US phone^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$Accepts mismatched brackets like (555 and rejects a leading +1.
US ZIP^\d{5}(-\d{4})?$Accepts 00000; the ZIP+4 group is capturing, so it shifts every later group number.
URL, rough^https?:\/\/[\w.-]+\.[a-z]{2,}(\/\S*)?$Rejects ports, userinfo, IDN hosts and IPv6 literals.
Hex colour^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$Nothing much — but note the alternation order matters if you drop the anchors.
Blank line^\s*$With m it matches per line; without it, only a wholly blank input. It also matches a line of non-breaking spaces.

None of these are production validators, and the third column is the point. Paste one in, then try to break it — the tester's whole value is in the inputs you did not think of. If a pattern is quietly matching invisible characters, run the text through the Unicode Converter to see what is actually in it.

When a live tester beats a unit test

The moment to reach for this is when you have a real sample — a log line, a CSV row, a webhook body — and a pattern that half works. Paste the sample whole rather than a trimmed version: trailing whitespace, a carriage return before the newline, and a byte-order mark at the head of a file are all invisible in an editor and all change what ^, $ and \s do.

It is also the fastest way to learn what a flag actually does, because you can toggle one character and watch the match count change. Turn m on and off with a multi-line sample and ^\d+; turn u on and off with an emoji and .. Reading the specification tells you the rule; watching the highlight move tells you the consequence.

What it cannot do is tell you how the pattern will behave under a different engine or under adversarial input. Before shipping, check the production failure modes guide, and if the pattern will run against untrusted text, test it against a long non-matching string first.

Frequently asked questions

Why does the match count include everything even when I leave the g flag out?

The tester adds g internally so it can enumerate. Your flags still control matching behaviour — i, m, s, u and v all apply — but the count is always the total in the text. In your own code, a pattern without g returns only the first match.

My pattern froze the page. What happened?

Almost certainly catastrophic backtracking: a quantifier wrapped around an already-quantified group, such as (a+)+$, against a long string that ultimately fails to match. The work is exponential in the input length and it runs on the page's main thread. Reload the tab and rewrite the inner group so it cannot match the same text two ways.

Will a pattern that works here work in Python, Go or Java?

Not necessarily. Go's regexp package is RE2-based and rejects backreferences and all lookahead and lookbehind outright. Python's \d matches Unicode digits by default while JavaScript's is ASCII-only. Java, PCRE and JavaScript all backtrack but differ on named group syntax and on some escapes.

What does the number after the @ in each match row mean?

The zero-based index where the match starts, counted in UTF-16 code units. A character above U+FFFF — most emoji — occupies two units, so offsets after one will be larger than the number of characters you can see.

Why does . match half of my emoji?

Because without the u flag the engine works in UTF-16 code units and an emoji is two of them. Add u to the flags box and the same pattern matches the whole character. u also enables \p{...} property escapes.

Is my test text uploaded anywhere?

No. The pattern is compiled and run by the browser's own RegExp engine inside the page. Nothing is transmitted, which is why you can safely paste a production log line into it.

Sources and further reading

Figures and definitions on this page are drawn from the following primary sources. If you find something out of date, tell us and we will correct it.

Related tools