This tester runs your pattern through the browser's own JavaScript RegExp engine as you type, highlighting every match inside your sample text and listing each one with its offset and any named-group captures. Because the engine *is* the browser, what you confirm here behaves identically in the .js or .ts file you paste it into — and by the same logic, this is not PCRE, not Python's re, and definitely not Go's RE2. Your pattern and sample text stay in this tab and are never transmitted.
How to use it
- Type the pattern body into the Regular Expression field. The
/characters on either side are decoration — do not paste a delimited literal like/\d+/g, or the slashes become characters you are searching for. You also write the source directly, so\dis enough; no doubling up as you would insidenew RegExp('\\d'). - Tick the flags you need:
gglobal (on by default),icase-insensitive,mmultiline (makes^and$anchor to each line),sdotall (lets.match\n). The grey text to the right of the field shows the active flag combination live. - Paste your sample into the Test Text box — multi-line log output, a CSV fragment, a blob of HTML. The box is resizable.
- A syntax error shows up immediately under the pattern field, verbatim from the engine: things like
Unterminated groupornumbers out of order in {} quantifier. That wording is the browser's, not ours. - Once the pattern compiles, the highlight panel reprints your sample with every match marked, and its header gives the total match count.
- The match list below itemises each hit: an index number, the matched text as a JSON string (so newlines and trailing spaces are visible as
\nrather than invisible), theindexoffset, and agroupsobject whenever you used(?<name>...). The list scrolls once it gets long.
When it comes in handy
Validate a pattern before it reaches your codebase
Pulling IPs out of Nginx access logs, checking a phone-number field, extracting issue IDs from commit messages — test it here and it will behave the same way in your bundle or your Node script, without a rebuild cycle for every character you change.
Pull structured fields out with named groups
Paste (?<date>\d{4}-\d{2}-\d{2}) (?<level>\w+) (?<msg>.*) and the groups column shows the field names next to their captured values on every row. Faster than logging match[1] and match[2], and it does not silently shift when you add a bracket in the middle later.
Work out why a pattern over- or under-matches
A greedy .* eating to end of line, alternation binding wider than you meant, a character class missing a hyphen — none of this is visible from reading the pattern. The highlight shows exactly where each match stops, and the index values reveal the gaps you are skipping between hits.
Check whether a borrowed pattern is even legal JavaScript
Regexes on Stack Overflow and in blog posts are often PCRE or Python. Paste one in and you find out straight away: (?P<name>...), (?>...), a++, and (?i) all fail in JavaScript with Invalid group or Nothing to repeat.
Limits and behavior
- Catastrophic backtracking will genuinely freeze the tab. JavaScript uses a backtracking engine with no linear-time guarantee like Go's RE2: on V8,
(a+)+bagainst 28acharacters takes roughly 18 seconds, and 32 characters roughly 38 — each extra character doubles it. This tool re-runs the match synchronously on every keystroke, with no timeout and no cancel button, so once you hit it your only option is closing the tab. Try nested quantifiers ((x+)*,(a|a)*,(\d+)+$) against very short strings first. - Only four flags are available:
g,i,m,s. There is nou/v(Unicode mode), noy(sticky), nod(hasIndices). The missinguis more than a missing checkbox: without it, Unicode property escapes like\p{L}or\p{Script=Han}do not error — they are parsed as the literal charactersp{L}and quietly match the wrong thing. - The match list shows named groups only. Numbered captures (
$1,$2) never appear, so if you need to see what a particular pair of brackets caught, rename it to(?<name>...). And becausegroupsis rendered withJSON.stringify, any named group that did not participate in the match has the valueundefinedand is dropped from the output entirely:/(?<a>x)|(?<b>y)/againstydisplays just{"b":"y"}. indexis a UTF-16 code-unit offset, not a character count. Emoji and other characters outside the BMP occupy two units each, so in👍bthebsits at index 2, not 1. For the same reason, without theuflag a single.consumes only half an emoji.\d,\w, and\bare ASCII-only. Full-width digits123do not match\d+,\w+will not touchabc, and\b\w+\bfinds nothing at all in Chinese, Japanese, or Korean text. For CJK you need explicit ranges such as[\u4e00-\u9fff]+.- This is a tester and nothing else: no replace preview, no pattern diagram, no library of common expressions, no export or copy button, and no persistence — refresh and the field is empty. Everything runs in the browser; the pattern and sample text are never sent to a server.
Frequently asked questions
Why does a pattern copied from Python or PHP fail here?
JavaScript supports a smaller grammar than PCRE. These are hard errors: Python-style named groups (?P<name>...) (JavaScript wants (?<name>...)), atomic groups (?>...), possessive quantifiers a++, inline flags (?i), comment groups (?#...), and recursion (?R). The nastier category is the constructs that compile but mean something else: \A and \Z are not anchors in JavaScript, just the literal letters A and Z, and the POSIX class [[:alpha:]] parses as a character set containing :, a, l, p, h followed by a stray ]. Neither raises an error — they just quietly stop matching, which is what the highlight panel is for.
Is lookbehind supported?
Yes. (?<=...) and (?<!...) work in V8 (Chrome, Edge) since 2018, Firefox 78, and Safari 16.4. JavaScript also allows variable-length lookbehind — (?<=ab+)c is perfectly legal, which is actually more permissive than Python's re, where the lookbehind must be fixed width. The trade-off is that such a pattern is an outright syntax error on older Safari, so check your browser support matrix before shipping it.
Why do I only get one match when I untick `g`?
That is deliberate: with g off, only the first match is reported, matching what a single re.exec(text) call gives you in code. Worth knowing about the related trap, though — if you reuse one g-flagged RegExp object across calls in your own code, lastIndex carries over, and a second test() on the same string can suddenly return false. This tool builds a fresh RegExp every time, so you will not see it here. Your code will.
The header says 5 matches but nothing is highlighted. Why?
Your pattern is matching zero-length strings — usually ^ with m, a \b, or a quantifier like a* that is happy to match nothing. Those appear in the match list as "" with an offset, but there are no characters for the highlighter to mark. It is also why the tool nudges the search position forward by one after a zero-length hit: without that, exec would loop forever at the same offset.
Does my test text get uploaded?
No. The whole tool is a new RegExp(pattern, flags) and an exec() loop running in your browser; there is no network call anywhere in the code. Disconnect from the internet and try it — it works exactly the same. One habit worth keeping anyway: when the sample is real customer log data, swap the sensitive fields for dummy values, because the pattern you end up copying elsewhere may travel with an example attached.
Why won't `$` match my last line, and why won't `.` cross a newline?
Two separate rules. Without m, JavaScript's $ matches only the very end of the string, and unlike Python's $ it does not tolerate one trailing newline: /abc$/ against "abc\n" is false, and only becomes true once you tick m. As for ., it excludes \n (and \r and the Unicode line separators) by default; tick s to let it through.