Regex cookbook and tester

Copy-paste patterns for the regex problems that come up constantly — email, URL, phone, IPv4, ISO dates, slugs, hex colours, UUIDs — each with a live tester below it. Paste your own pattern instead and get live match highlighting, numbered and named capture groups, a substitution preview, and a plain-English explanation of what every token means. Your pattern and test text run and stay in this tab.

/ / g
Flags

    Your pattern and test text never leave this tab. Matching runs in a same-origin Web Worker — a background thread this page starts, not a server request — so nothing you type is ever sent anywhere to be tested. That is also what stops a runaway pattern from freezing the page: see how the catastrophic-backtracking guard works →

    A bad regex is a production outage, not a typo

    It is easy to treat a regular expression as a small, contained thing — a pattern you write once, glance at, and ship. The clearest counterexample on record is a postmortem Cloudflare published about its own network, and it is worth reading in full rather than taking secondhand.

    On 2 July 2019, a single rule deployed to Cloudflare’s Web Application Firewall contained a regular expression with the classic catastrophic-backtracking shape — a repeated group whose own contents could also repeat, so the engine tried an exponential number of ways to fail a match before giving up. Because a WAF rule runs against every request passing through, that one pattern drove CPU usage toward 100% across Cloudflare’s global network almost immediately. With core proxy, CDN and WAF capacity pegged, the network served 502 errors to a large share of its traffic for roughly 27 minutes before the rule was pulled.

    Two contributing factors turned a bad pattern into a global incident rather than a contained one. The rule went out everywhere in one step, with no staged or canary rollout that would have surfaced the CPU spike on a slice of traffic before it reached all of it. And a CPU-usage safeguard that had once existed specifically to catch a runaway regular expression before it consumed a whole core had been removed in an earlier, unrelated refactor. Either mitigation alone — the staged rollout, or the reinstated CPU guard — would likely have kept the incident local. The regex engine involved matched by backtracking with no runtime guarantee against this class of input, which is not a defect unique to that one engine: most regex engines in everyday use, including PCRE, Python’s re and JavaScript’s own RegExp, share the same backtracking design and the same exposure.

    This is the general class of bug called ReDoS — regular expression denial of service — and OWASP’s page on it is a good next stop. The vulnerability is not a mistake in one pattern’s logic so much as a structural property some patterns have: ordinary test input never exercises it, and adversarial — or simply unlucky — input can trigger it by accident. That is exactly why this page runs every match inside a Web Worker with a hard deadline it can terminate, rather than directly on the page’s main thread; see how that timeout actually works above.

    A pattern that looks fine against the three test strings you tried by hand can still be the one that hangs on the fourth. Testing a pattern against realistic — and deliberately adversarial — input before it ships is the cheap step that catches this, and it is what the tester on this page is for.

    Read the reporting yourself

    Copy-paste pattern library

    Nine ready patterns for the problems that come up again and again. Every one targets JavaScript’s own regex flavour — see flavour honesty below for exactly where that differs from PCRE or Python. Click Try it to load a pattern straight into the tester above with its own worked example, or Copy to take just the pattern.

    Flavour honesty: this is JavaScript’s regex, not PCRE or Python’s

    Every pattern on this page runs through JavaScript’s built-in RegExp. That flavour is close to, but not identical with, the PCRE engine behind PHP and most command-line grep-style tools, or Python’s re module. Copying a pattern between them without adjustment is a common source of “this worked everywhere except my browser” bugs. Specifically:

    • Named groups. JavaScript spells a named capturing group (?<name>…). Python spells the same thing (?P<name>…). Pasting Python’s form into a JavaScript RegExp throws a SyntaxError — this page will show you that error rather than silently failing.
    • Lookbehind. (?<=…) and (?<!…) are supported in current Chrome, Firefox and Edge, and in Safari from version 16.4 (March 2023). A pattern using lookbehind will throw in older Safari and in some embedded JS engines. PCRE and Python have supported lookbehind for much longer, so a pattern working in a Python script may still fail in an old browser.
    • No possessive quantifiers, no atomic groups. PCRE offers a++ (possessive) and (?>…) (atomic) specifically to cut off backtracking early, which is also a standard defence against catastrophic backtracking. JavaScript has neither. That is one reason a pattern that is safe in PCRE can still be dangerous here — see the ReDoS section below.
    • No recursion. PCRE supports recursive patterns ((?R)) for matching nested structures such as balanced parentheses. JavaScript regex cannot recurse at all; balanced-nesting problems need a real parser, not a pattern.
    • \d, \w and Unicode. Without the u flag, JavaScript’s shorthand classes are ASCII-only, same as PCRE’s default. With u, JavaScript still does not expand \d to every Unicode decimal digit — for that you need the explicit Unicode property escape \p{Nd}, which requires the u (or v) flag and is not available in every engine that claims Unicode support.

    None of this makes JavaScript’s regex worse — it is simply a different implementation of a specification (ECMA-262) that overlaps heavily with, but is not identical to, PCRE or Python’s re. The patterns in the library above are written to work as JavaScript regex specifically, and the tester on this page only ever compiles and runs JavaScript regex — if you paste a PCRE- or Python-only construct, you will see the SyntaxError immediately rather than a confusing runtime difference later.

    How it works

    Compiling a pattern

    Every pattern you type is handed to new RegExp(pattern, flags), wrapped in a try/catch. Constructing a RegExp from bad syntax — an unmatched [, a dangling (, a quantifier with nothing before it — throws a SyntaxError, and without the wrapper that exception would be unhandled and the page would break. Here it becomes the message you see under the pattern field instead: the engine’s own words for what is wrong, immediately, with no separate “validate” step required.

    Stopping a runaway pattern from freezing the tab

    A hand-written regex tester has one failure mode that matters more than any other: catastrophic backtracking. The textbook example is /(a+)+$/ against a string of many as followed by a character that can never match, such as aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!. The inner + and the outer + both repeat, so the engine tries an exponential number of ways to divide the same run of as between them before it can conclude there is no match. On a string of thirty characters that is already billions of attempts; add a few more characters and it stops being something you wait out.

    This page defends against that in three layers, and it is worth being precise about what each one actually does, because two of the obvious-sounding defences do not really work:

    • A heuristic warning, before running anything. The pattern text is scanned for the textbook shape — a group that can itself repeat, wrapped in another repetition, like (a+)+ or ([a-z]*)*. If it matches, you see a warning badge before the tester runs the pattern at all. This is advisory only: it will miss some genuinely catastrophic patterns (the danger can hide behind alternation or backreferences a simple text scan cannot see) and could in principle flag a rare safe one, so it is a hint, never a block.
    • A capped iteration count — but this is not the real fix. The match loop stops after 20,000 iterations and reports that it was truncated. It is tempting to assume this is what prevents a freeze, but it is not: catastrophic backtracking happens inside a single call to the engine’s match function, before that call ever returns to the loop that is counting iterations. Capping the number of loop iterations only helps a different, unrelated problem — a pattern that matches validly but produces an enormous or endless number of matches (for instance, a pattern that can match a zero-length string, repeated across a very long input).
    • The actual fix: a Web Worker with a timeout. Matching does not run on the same thread as the page you are looking at. It runs inside a same-origin Web Worker — a background script this page starts, loaded from a file next to this one, not a network request. The tester starts a timer the moment it sends a pattern to the worker. If the worker has not replied by the deadline, the page calls Worker#terminate(), which kills the worker outright — even though it is still stuck deep inside a native RegExp#exec call that will never return on its own — and a fresh worker is created for your next attempt. Terminating a worker does not touch the main thread at all, which is precisely why the rest of the page keeps responding to clicks and typing throughout. That is the property that actually matters: not that the runaway match finishes quickly (it may never finish), but that killing it is always possible and never blocks anything else.

    There is a smaller, easier-to-fix trap in the same neighbourhood: a pattern that can match a zero-length string with the global flag on, such as /x*/g against text with no x in it. A naive loop that calls exec() and only relies on the engine to advance its own position can match the same empty string at the same index forever. This page’s matching loop checks the length of every match and manually advances lastIndex by one whenever a match is zero-length, which is the standard, documented way to make that loop terminate.

    Explaining a pattern in plain English

    The explainer is a second, completely separate pass over the pattern’s text — it never compiles or runs the pattern, so it carries none of the risk described above, no matter how pathological the pattern looks. It walks the pattern character by character, recognising anchors, character classes and their shorthand forms, groups (capturing, non-capturing, named, lookahead, lookbehind), backreferences, and the four quantifier shapes with their greedy and lazy variants, and produces one row per token: the raw text on the left, a sentence on the right. It is a hand-written scanner rather than a full parser, so a handful of exotic constructs get a generic description instead of a precise one — an accepted trade-off for explaining the common 95% of patterns clearly, which is what a cookbook is for.

    Substitution

    The replace tab supports the same template syntax JavaScript’s own String.prototype.replace understands: $& for the whole match, $1$99 for numbered groups, $<name> for named groups, and $$ for a literal dollar sign. It is reimplemented here rather than calling .replace() directly so that substitution goes through the exact same capped, manually-advanced matching loop as the Matches tab — one ReDoS story for both features, not two, and both run inside the same worker with the same timeout.

    Never an unhandled error, never raw HTML from your input

    Two more things worth stating plainly. First: every operation on this page — compiling, matching, explaining, substituting — is wrapped so that a bad pattern or bad input produces a message, never a crash. Try [, ( or a bare * in the pattern field and you get the engine’s own explanation, not a blank page. Second: every value that reaches the screen from your pattern or your test text — the highlighted matches, the match list, the explanation, the substitution result — is written with textContent, never assembled into an HTML string. Paste <img src=x onerror=alert(1)> as test text and it renders as those literal characters, because there is no code path on this page that turns your text into markup.

    Worked examples

    1. Pulling structured data out of free text with named groups

    You have a log line like 2026-07-22 14:03:11 ERROR payment-service: timeout after 5000ms and you want the date, the level and the service name, not just a yes/no match. Named groups turn a single match into a small object:

    ^(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2}) (?<level>[A-Z]+) (?<service>[\w-]+):

    Paste that pattern here with the log line as test text and the match list shows four named captures — date, time, level, service — each with its own value, instead of one long numbered match you would have to count positionally. In code, match.groups.service reads far better than match[4], and it survives someone inserting a new group earlier in the pattern without silently renumbering everything downstream.

    2. Why .* is greedy, and when that bites you

    Given the HTML fragment <b>bold</b> and <i>italic</i>, the pattern <.*> does not match <b> the way most people expect. * is greedy by default — it consumes as much as it can and only gives characters back if the rest of the pattern cannot match otherwise — so it grabs everything up to the last > in the string, matching the entire fragment from <b> to </i> as one result.

    Add ? after the quantifier to make it lazy — <.*?> — and it takes the smallest match that still lets the rest of the pattern succeed, so it stops at the first > it meets: four separate matches, <b>, </b>, <i>, </i>. Try both patterns here against the same text and watch the highlight change from one long span to four short ones. The deeper lesson usually matters more than the fix: a general .* across angle brackets is fragile on real HTML regardless of greediness — nested tags, attributes containing > inside a quoted string, and comments all break it. For anything beyond a fixed, known fragment, parse the HTML with a real parser (the DOM, in a browser) rather than a regex.

    3. A validator that is deliberately not exhaustive

    The email pattern in the library, ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$, rejects "quoted string"@example.com, which RFC 5322 technically allows, and accepts a@b.co, which nothing stops you registering but which will bounce every mail server ever built for it. Both are correct behaviour for this pattern’s actual job: catching obvious typos in a signup form, not proving deliverability.

    The only way to know an email address actually works is to send a message to it and see what comes back — ideally a confirmation link the person has to click. Treat every email regex, including this one, as a typo filter with a known false-negative rate on the exotic 1% of technically-legal addresses, not as proof the address is real. The dedicated email-validation page goes through the trade-off in more detail, including why + in the local part deserves special handling in your application code even after the regex accepts it.

    Reference: tokens, quantifiers, anchors, classes and flags

    Character classes

    TokenMatches
    .Any character except a line break (unless the s flag is set)
    \dA digit, 09
    \DAny character that is not a digit
    \wA word character: letter, digit or underscore
    \WAny character that is not a word character
    \sA whitespace character: space, tab, newline and friends
    \SAny character that is not whitespace
    [abc]Any one of a, b or c
    [^abc]Any character except a, b or c
    [a-z]Any character in the range a to z

    Quantifiers

    TokenMeaning
    *Zero or more, as many as possible (greedy)
    +One or more, as many as possible (greedy)
    ?Zero or one — makes the preceding token optional
    {n}Exactly n times
    {n,}n or more times
    {n,m}Between n and m times, inclusive
    *? +? ??Lazy variants — as few repetitions as possible

    Anchors and boundaries

    TokenMeaning
    ^Start of the string, or of a line with the m flag
    $End of the string, or of a line with the m flag
    \bA word boundary — between a word character and a non-word character
    \BAnywhere that is not a word boundary

    Groups and lookaround

    TokenMeaning
    (…)Capturing group — remembers what it matched, numbered from 1
    (?:…)Non-capturing group — groups for the quantifier, but is not remembered
    (?<name>…)Named capturing group
    (?=…)Lookahead — must be followed by this, but it is not part of the match
    (?!…)Negative lookahead — must NOT be followed by this
    (?<=…)Lookbehind — must be preceded by this (Safari 16.4+)
    (?<!…)Negative lookbehind — must NOT be preceded by this
    \1Backreference to capturing group 1
    \k<name>Backreference to the group named “name”
    |Alternation — either the part on the left or the part on the right

    Flags

    FlagNameEffect
    gGlobalFind every match instead of stopping at the first
    iIgnore caseUppercase and lowercase letters match each other
    mMultiline^ and $ also match at every line break
    sDot-all. also matches line breaks
    uUnicodeTreats the pattern as Unicode code points, not UTF-16 code units, and enables \p{…}
    yStickyOnly matches starting exactly at lastIndex, with no scanning forward

    Frequently asked questions

    Is my pattern or test text sent anywhere?

    No. Compiling, matching, explaining and substituting all run as JavaScript inside this tab — the matching step specifically runs inside a same-origin Web Worker, which is still just a background thread of this same page, not a network destination. There is no upload endpoint on this page and no code path capable of sending what you type anywhere.

    You do not have to take that on faith. Open developer tools (F12, or I on a Mac), select the Network panel, type a pattern and some test text, and watch the request list stay empty.

    Does testing a pattern here send it anywhere?

    No. This page only ever tests JavaScript’s own RegExp, which every browser already ships, so there is nothing that needs a server round-trip — and there is no save or share feature, so there is nowhere for a pattern to be stored even in principle. Testing one flavour is a deliberate scope limit rather than an oversight: a tester covering PCRE, .NET, Python or Go has to reach an engine the browser does not have. If you are evaluating any online tool for sensitive input, the reliable check is the same everywhere — open DevTools, watch the Network tab while you type, and see for yourself what does and does not leave the page.

    What is catastrophic backtracking and could my pattern trigger it?

    It is what happens when a regex engine has to try an exponential number of ways to match the same input, usually because a repeated group contains something that can itself repeat — (a+)+, ([a-z]*)*, (\d+){2,} are the classic shapes. Against the right adversarial input — a long run of the repeated character followed by something that can never satisfy the rest of the pattern — matching can take longer than the age of the universe on ordinary hardware. See the full explanation and mitigation above; the short version is that this page warns on the textbook shape and, more importantly, runs matching in a background worker with a timeout so a hang here can never freeze the tab.

    Why did my pattern match one fewer or one more time than I expected?

    The two usual causes are the g flag and greediness. Without g, a pattern only reports its first match, ever — toggle the g chip on to see every occurrence. With greediness: an unqualified * or + consumes as much as it can before backtracking, which can make one large match swallow several things you expected to see as separate matches. Add ? after the quantifier for the lazy variant, which takes the smallest match that still lets the rest of the pattern succeed — see worked example 2 above for exactly this, with the highlight to compare both ways.

    My pattern works in Python or PHP but throws a SyntaxError here. Why?

    Almost always a named-group spelling mismatch: Python’s re and PHP’s PCRE both accept (?P<name>…), while JavaScript requires (?<name>…) — no P. The second most common cause is a possessive quantifier (a++) or an atomic group ((?>…)), neither of which JavaScript’s regex engine implements at all. See flavour honesty above for the full list of differences worth knowing before you copy a pattern across languages.

    Can this validate against a full specification, like RFC 5322 for email?

    No, and neither should you want it to for most purposes. A regex that fully implements RFC 5322’s email grammar is hundreds of characters long, accepts addresses no real mail server does in practice (quoted strings, comments, IP-literal domains), and still cannot tell you whether the mailbox exists. The library’s email pattern, like most production validators, targets the common 99% case — catching obvious typos — and treats an unreachable inbox as a problem for a confirmation email to catch, not a regex. See worked example 3 above.

    Does the highlighting understand overlapping matches?

    No — and neither does JavaScript’s own regex engine. Once a match consumes a run of characters, scanning continues from the end of that match, so a second match cannot start inside characters the first one already used. If you need overlapping matches (every position where a shorter pattern could start, even inside a longer match found first), you have to re-run the search starting one character after each previous start rather than after each previous end — JavaScript’s built-in matching does not offer this as an option, and neither does this page.

    What happens with a zero-length match, like \b or x*?

    It is reported like any other match, with identical start and end positions and an empty matched string. The one thing that has to be handled carefully is advancing past it: with the g flag, a naive loop that lets the engine manage its own position can get stuck reporting the same empty match at the same index forever. This page’s matching loop detects a zero-length match and advances one character past it manually, which is the standard fix and the reason the match list here always terminates.

    Is the pattern explainer always exactly right?

    For the constructs covered in the reference tables above — anchors, character classes, shorthands, the four quantifier shapes, groups, lookaround, backreferences — yes. It is a hand-written scanner rather than a complete regex parser, so a small number of exotic constructs (deeply nested Unicode property escapes, unusual flag combinations) fall back to a generic description rather than a precise one. That trade-off favours explaining the patterns people actually paste clearly, over parsing every legal but rare construct.

    Does it work offline, once the page has loaded?

    Yes. After the initial load — including the worker script, which is a same-origin file rather than a remote fetch — nothing further is requested. Save the page with Ctrl/S and matching, explaining and substitution keep working with the network disconnected.

    Use cases and limitations

    What people use this for

    • Validating form input — email shape, phone shape, a slug for a URL — before it reaches a server, as a first-line typo catcher, not a proof of correctness.
    • Pulling fields out of semi-structured text — log lines, CSV-ish exports, config values — using named groups to get a small object back instead of a yes/no match.
    • Bulk find-and-replace across text that follows a pattern but is not identical, such as normalising date formats or redacting anything that looks like a phone number, using the Replace tab’s $1/$&/$<name> template.
    • Learning what an unfamiliar pattern does — paste a regex you found in someone else’s code into the Explain tab before you trust it in production.
    • Checking a pattern is safe before shipping it — the catastrophic-backtracking warning catches the textbook danger shapes before a pattern goes anywhere near a server that might not have this page’s worker-timeout protection.

    What it deliberately does not do

    • Test PCRE, Python, .NET or any other flavour. One engine, the one already in your browser. See flavour honesty.
    • Guarantee a pattern is ReDoS-free. The heuristic warning catches the textbook shape, not every possible catastrophic construct — the worker timeout is what actually protects the tab, on every pattern, whether or not the heuristic recognised it in advance.
    • Parse HTML, JSON or any nested, recursive structure. Regex has no general answer to balanced nesting; that needs a real parser. Worked example 2 above walks through exactly this trap.
    • Prove an email, phone number or URL is real. Syntax only — a validator that also confirmed deliverability or reachability would need to make an actual network request, which is out of scope for a page that runs entirely client-side.
    • Find overlapping matches. Neither does JavaScript’s engine; see the FAQ above.

    Honest limitations

    The catastrophic-backtracking heuristic is advisory and incomplete by design — it recognises one well-known shape and will miss others, particularly ones hidden behind alternation or backreferences. The worker timeout is the real safety net and does not depend on the heuristic recognising anything. The plain-English explainer is a hand-written scanner, not a complete formal grammar for every legal JavaScript regex construct; it covers the constructs in the reference tables precisely and falls back to a generic description for the rest. Very large test strings (above roughly 200,000 characters) may be slow to highlight in full even when the pattern itself is safe, simply because building a match list and a highlighted view over that much text takes real work; the iteration cap keeps this bounded rather than unbounded, at the cost of a truncation notice on genuinely enormous inputs.

    More tools on this site

    See all the tools on this site →