Which regex dialect this is
This page compiles your pattern with new RegExp(pattern, flags), so what you see is precisely how it behaves in JavaScript, TypeScript, and Node. That matters because regex is not one language. PCRE (PHP, and grep -P), POSIX ERE (grep -E, sed -E), Python's re, and Go's RE2 all differ in ways that will silently change your results.
- Lookbehind —
(?<=…)— works in modern JavaScript, PCRE, and Python, but not in Go's RE2 or ingrep -E. - Atomic groups and possessive quantifiers —
(?>…),a++— exist in PCRE and not in JavaScript. This is unfortunate, because they are the cleanest defence against catastrophic backtracking. - `\d` and `\w` are ASCII-only in JavaScript by default. With the
uflag and\p{…}you get Unicode properties:\p{L}for any letter,\p{N}for any number. - RE2 (Go, and Google's tooling) has no backtracking at all, which makes it immune to the exponential blowup described below — but it also has no backreferences or lookaround.
- POSIX ERE has no
\d, no non-greedy quantifiers, and no lookaround.grep -E '\d+'does not do what you expect;grep -E '[0-9]+'does.
So a pattern verified here is verified for JavaScript. Before pasting it into a grep command or a Go service, check that every construct exists in that dialect.
The flags
| Flag | Name | Effect |
|---|---|---|
g | global | Find every match rather than stopping at the first. Without it, String.replace replaces only once. |
i | ignoreCase | Case-insensitive matching. |
m | multiline | Makes ^ and $ match at line breaks instead of only at string boundaries. |
s | dotAll | Makes . match newlines too. Essential for matching across lines in a log or an HTML blob. |
u | unicode | Correct handling of code points above the BMP and enables \u{…} and \p{…}. |
y | sticky | Matches only at lastIndex — used by tokenizers, rarely useful interactively. |
Read the full explanation of regex tester results ↓Show less ↑
The g flag has a sharp edge worth knowing about. A global regex object carries mutable state in its lastIndex property, so calling .test() twice on the same regex with the same string alternates between true and false. Reusing a module-level const RE = /x/g across requests is a genuine and very confusing bug source. Either create the regex where you use it, or reset lastIndex to 0 before each run.
The m flag is the one people reach for when they mean s. m changes what ^ and $ anchor to; s changes whether . crosses a newline. If your pattern fails on multi-line input, you almost certainly want s.
Capture groups
Parentheses do two jobs: they group for quantifiers, and they capture for extraction. When you only need the grouping, use a non-capturing group (?:…). This is not just tidiness — every capturing group has a cost, and more importantly it shifts the numbering of every group after it, which quietly breaks code that reads $2.
Named groups solve that problem properly: (?<year>\d{4})-(?<month>\d{2}) gives you match.groups.year regardless of position. This tool shows both the numbered and the named columns in its results table, so you can see exactly what your code will receive.
A group that did not participate in the match is undefined, not an empty string. /(a)|(b)/ matching against "b" gives $1 === undefined and $2 === "b". Code that does match[1].trim() on an optional group throws, and the table on this page distinguishes undefined from empty for exactly that reason.
Greedy, lazy, and the backtracking that follows
*, +, and ? are greedy: they consume as much as possible, then give characters back until the rest of the pattern can match. Adding ? makes them lazy — .*? takes as little as possible and expands only as needed. The classic demonstration is matching an HTML tag: <.*> against <b>bold</b> matches the entire string, because the greedy .* runs to the end and backtracks only to the *last* >. <.*?> matches just <b>.
Better than either is to match precisely what you mean: <[^>]*> cannot overshoot at all, needs no backtracking, and is faster. As a general rule, a negated character class beats a lazy quantifier whenever you can express the stop condition as "any character except X".
Catastrophic backtracking, and why this tool has caps
JavaScript's regex engine is a backtracking engine, and backtracking can go exponential. The canonical example is ^(a+)+$ tested against a string of 30 a characters followed by a !. There is no match, but the engine must try every way of partitioning those 30 characters between the inner and outer quantifiers before it can conclude that — roughly 2³⁰ attempts. Add one more a and it doubles.
A JavaScript regex cannot be interrupted once it starts, so a pattern like that freezes the tab or, on a server, pins a CPU core and stops answering requests. That is ReDoS — a denial of service triggered by a single small input. It is a real and regularly exploited vulnerability class, most often in user-supplied search filters and in validation regexes for email addresses and URLs.
The defences available inside a browser page are limited, and this tool uses all of them: the test string is capped at 20,000 characters, the match count is capped, compilation and execution are wrapped in try/catch, and a run taking more than 250 ms is reported with a warning. It cannot prevent a pathological pattern from being slow — nothing in-page can — but it makes the problem visible before you deploy it.
(a+)+, (a*)*, (.*)* — and alternations where the branches can match the same text, like (a|a)*. If your pattern contains one and runs against input a user controls, rewrite it before it ships.The fixes are structural. Make the inner and outer quantifiers unambiguous so there is only one way to match. Replace .* with a negated class that cannot cross the delimiter. Anchor the pattern so the engine cannot retry from every starting offset. And bound the input length before matching — most ReDoS is only reachable because a 100 KB string was allowed into a validator that expected 50 characters.
Command-line equivalents
# ERE — no \d, no lazy quantifiers, no lookaround
grep -E '[0-9]{1,3}(\.[0-9]{1,3}){3}' access.log
# PCRE via grep -P — closest to JavaScript; -o prints just the match
grep -Po '(?<=status=)\d+' app.log | sort | uniq -c
# ripgrep uses Rust's regex crate: no backtracking, so no ReDoS,
# but also no backreferences or lookaround
rg '\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b' access.log
# sed with ERE, capturing and reordering
sed -E 's/^([^ ]+) ([^ ]+)/\2 \1/' file.txt
# Test a JavaScript regex exactly as this page does, from a shell
node -e 'const re=/\b\d{1,3}(?:\.\d{1,3}){3}\b/g;
console.log("host 10.0.0.1 and 192.168.1.7".match(re))'
# -> [ '10.0.0.1', '192.168.1.7' ]
# Extract a DKIM selector from a mail header
grep -Eo 's=[^;]+' headers.txtThat last example is a good reminder of scope. Regex is excellent at pulling a token out of a line of text and poor at parsing structured formats. Do not parse JSON with it — use the JSON formatter or jq. Do not validate email addresses with it either; the RFC 5322 grammar is genuinely not regular, every "complete" email regex you find is wrong somewhere, and the only reliable test is to send a message and see whether it is accepted. Check for an @, check the domain has an MX record, and let the mail server be the judge.