Regex Tester

Test JavaScript regular expressions live: match highlighting, capture group table, named groups and replace preview — with a catastrophic-backtracking guard.

Runs in your browser

JavaScript (ECMAScript) flavour. Your pattern and test string stay after the # and are never sent.

/ /g
Starter patterns

Your pattern and test string live after the # in the address bar. Browsers never send the fragment in an HTTP request, so it cannot reach our logs, a CDN’s or a proxy’s.

Result

Type a pattern to see matches.

Cheat sheet
.Any character except a newline
\dA digit (0–9)
\wA letter, digit or underscore
\sAny whitespace
[abc]Any one of a, b or c
[^abc]Any character except a, b or c
[a-z]Any character in the range
*Zero or more of the previous item
+One or more of the previous item
?Optional — zero or one
{2,4}Between 2 and 4 of the previous item
*?Lazy — match as few as possible
(…)Capture group
(?:…)Group without capturing
(?<n>…)Named capture group
a|bEither a or b
^Start of the string (or line with m)
$End of the string (or line with m)
\bWord boundary
(?=…)Lookahead — followed by
(?!…)Negative lookahead — not followed by
(?<=…)Lookbehind — preceded by
\1Back-reference to group 1
\p{L}Unicode property (needs the u flag)

The ECMAScript flavour and its limits

Patterns here run through the browser’s own RegExp engine, so what you see is exactly what JavaScript does — the flavour defined in ECMA-262 and used by Node, Deno, TypeScript and every browser. That includes lookahead and lookbehind ((?=…), (?<=…)), named groups ((?<year>…)), backreferences and Unicode property escapes such as \p{L} or \p{Script=Hangul} when the u flag is on.

What ECMAScript does not have: atomic groups (?>…), possessive quantifiers (a*+), recursion (?R), conditionals, \A and \z anchors, and the free-spacing x flag. A pattern copied from PHP’s PCRE, Python’s re or .NET may therefore need small edits — most often replacing (?P<name>…) with (?<name>…).

Supporting those other flavours would mean running your pattern and your test data — usually a slice of production logs — on someone’s server. Naming the limit is the honest trade: everything on this page stays in the tab.

What each flag changes

  • g (global): find every match instead of stopping at the first. Without it the match list has at most one entry, and replace changes only the first occurrence.
  • i (ignore case): ASCII and Unicode case folding, so /straße/i matches STRASSE only under u.
  • m (multiline): ^ and $ match at every line break, not just at the start and end of the whole string.
  • s (dotAll): . also matches a newline. Without it, . stops at line ends — the usual reason a pattern fails on multi-line text.
  • u (unicode): treats the pattern and input as code points, which is what makes \p{…} work and stops emoji and rare CJK characters from being split into surrogate halves.
  • y (sticky): matches only at lastIndex — used by tokenizers, rarely useful for searching.
  • d (indices): records the start and end offset of every group. We switch it on regardless, because it is what lets the highlighting colour each capture group inside a match.

Reading the match table

Every match shows its start and end offset, the matched text, each numbered group and each named group. Numbering follows the opening parentheses from left to right, so nesting counts: in /((a)(b))/ group 1 is "ab", group 2 is "a" and group 3 is "b". Use (?:…) when you want grouping without a capture.

A group that exists in the pattern but did not take part in the match is shown as undefined — different from an empty string, and a distinction that matters when you index into the result in code. Alternation is the usual source: /(a)|(b)/ always leaves one of the two undefined.

Matching stops at 10,000 results; above that the table says how many exist in total. That is a display limit, not a correctness one — the count you see is real.

Replacement syntax

In the Replace tab the replacement string follows String.prototype.replace: $1 … $99 insert numbered groups, $<name> inserts a named one, $& is the whole match, $` and $' are the text before and after it, and $$ is a literal dollar sign. Everything else is copied through untouched.

Two habits save time. Reorder with groups instead of writing two patterns — /(\d{4})-(\d{2})-(\d{2})/ with $3/$2/$1 turns 2026-09-06 into 06/09/2026. And remember that without the g flag only the first match is replaced, which is the most common "why did only one change" question.

Catastrophic backtracking (ReDoS)

A backtracking engine tries alternatives until one fits. Nest one quantifier inside another — (a+)+$, (\s*,)*, (\w|\d)*@ — and the number of ways to split the input grows exponentially, so a 30-character string can take longer than the age of the universe to reject. The same pattern on a server is a denial-of-service bug, catalogued as CWE-1333.

Every match here runs inside a Web Worker with a two-second watchdog: if the pattern has not finished, the worker is terminated and you get a timeout message instead of a frozen tab. The pattern box also warns when it spots the nested-quantifier shape, before you run it.

The fix is almost always to make the inner part unambiguous: replace (a+)+ with a+, anchor the pattern, use a negated character class ([^"]*) instead of a lazy .*?, and split the work into two simpler passes rather than one clever expression.

Gotchas that look like engine bugs

\w means [A-Za-z0-9_] and \d means [0-9] — ASCII only. \b is defined in terms of \w, so it does not sit between Korean or accented words the way you expect: /\b한국\b/ behaves oddly because 한 is not a word character. Use \p{L} with the u flag, or lookarounds on whitespace, for non-ASCII text.

A dot inside a character class is literal: [.] needs no escape, while . outside one matches almost anything. And a global regex object carries lastIndex between calls, so reusing one across .test() calls gives alternating true and false — the reason this tool resets it before every run.

Finally, some things regex cannot do well. Nested structures — HTML, brackets, JSON — need a parser, not a pattern; and no regular expression fully validates an email address as RFC 5322 defines it. The built-in starter patterns are pragmatic filters, not specifications.

Sharing a pattern without leaking data

Share link puts the pattern, the flags and the test string after the # of the URL. Browsers never include the fragment in an HTTP request, so the shared data cannot appear in our access log, a CDN’s, a proxy’s or a referrer header — unlike the query-string sharing other testers use.

The link still travels wherever you send it, so treat a shared URL like the log sample it contains. Your working pattern and text are also kept in this browser’s localStorage so a reload does not lose them; Clear removes both.

How to use

  1. Type your pattern between the / / marks, or click a starter such as Email, IPv4 or Semver.
  2. Toggle the flag buttons g, i, m, s, u, y and d — each one names what the flag does.
  3. Paste your text into ‘Test string’ — matches are highlighted and listed with their groups.
  4. Switch to the Replace tab and write a replacement using $1 or $<name> to preview the output.
  5. Use ‘Copy pattern’, ‘Copy matches (JSON)’ or ‘Share link’ to take the result with you.

FAQ

Why only the JavaScript flavor?

Because JavaScript is the only flavor a browser can run. PCRE, Python and .NET flavors require a server, which means your pattern and your test data — often production log samples — get transmitted. We would rather be honest about the limit than send your data away.

Is my test string in the share link sent anywhere?

No. We put it after the #, and browsers never send the fragment in an HTTP request. It cannot appear in our access logs, a CDN’s, or a proxy’s. Query-string sharing, which other tools use, does end up in logs.

What is catastrophic backtracking?

Some patterns — typically nested quantifiers like (a+)+$ — make the engine try exponentially many combinations, freezing the tab. We run every match in a Web Worker with a 2-second watchdog and stop it instead of letting your browser hang.

Why does my pattern match nothing across several lines?

Two flags do different jobs and people reach for the wrong one. s (dotAll) makes . match a newline, which is what you need for a pattern that spans lines. m (multiline) only changes ^ and $ so they match at every line break. A pattern like /start.*end/ needs s, not m.

Can I paste a pattern as /foo/gi, the way it appears in code?

Yes. A full literal with slashes and trailing flags is recognised: the body goes into the pattern box and the flags are switched on for you. It is the fastest way to reproduce a regex you found in a code review or a stack trace without hand-picking the flag buttons.

Related tools

More Developer tools: Developer

Send feedback

Found a bug or have an idea? Tell us — it helps make the tools better.

Only needed if you would like a reply.

Page: /developer/regex-tester/