Regex Tester
Type a regex and sample text to see matches and groups in real time.
Compile a pattern, run it against sample text, and inspect what actually matched. This runs on the browser's own JavaScript engine, which matters in two ways: a pattern that behaves one way here behaves differently in PCRE, RE2 or POSIX, and a pattern that is instant on your sample can hang a server on an input twenty characters longer.
Nested quantifiers and the inputs that take exponential time
Backtracking engines — JavaScript, PCRE, Perl, Python, Java and .NET by default — try alternatives in order and undo them on failure. When one quantifier is nested inside another, the number of ways to divide the input between them grows exponentially, and proving that no arrangement matches means trying all of them.
^(a+)+$ against thirty a characters followed by an exclamation mark is the canonical demonstration. There is no match, and the engine has to test roughly 230 partitions of those thirty characters — a little over a billion attempts on a 31-character string. Add one more character and it doubles.
The shapes to watch for are a quantifier applied to a group that already contains one ((a+)+, (\d+)*), an alternation whose branches can match the same text ((a|a)+, (\s|\s\s)+), and a .* followed by something that can also consume the same characters.
These take down real systems. Stack Overflow was offline for 34 minutes on 20 July 2016 when a trailing-whitespace regex met a post containing roughly 20,000 consecutive space characters. Cloudflare took its global network down for about half an hour on 2 July 2019 after deploying a WAF rule containing .*(?:.*=.*), which pinned CPU on every machine in the fleet.
In Node the blast radius is larger than one request: matching is synchronous and blocks the event loop, so a single crafted input stalls the whole process. Test with adversarial input rather than representative input — take the string your pattern is supposed to reject and lengthen the part the quantifiers overlap on.
Greedy, lazy, and the atomic groups JavaScript does not have
.* consumes everything and hands characters back one at a time; .*? consumes nothing and takes them one at a time. Both can reach the same match and both can backtrack, so switching greedy to lazy changes which match you get and often how long it takes to find, but it does not make a pattern safe.
The construct that does make it safe is one JavaScript lacks. An atomic group (?>...) and a possessive quantifier a*+ both tell the engine to commit: once the group has matched, it will never give characters back, which collapses the exponential search. Java, PCRE2 and Ruby support both, Python gained both in 3.11, and .NET supports atomic groups but not possessive quantifiers. JavaScript supports neither, in any current version.
The standard JavaScript workaround is a lookahead with a capture: (?=(a+))\1 behaves like a possessive a++. It works and it is unreadable, which is itself a signal to ask whether the job needs a backtracking engine.
Where you control the runtime, the structural fix is an engine that cannot backtrack. Go's regexp and Rust's regex crate are RE2-style: they guarantee time linear in the length of the input, and they pay for it by refusing to support backreferences and lookaround at all. .NET 7 added RegexOptions.NonBacktracking with the same trade-off, and .NET has accepted a matchTimeout since 4.5. Java's Pattern offers neither. PHP takes a third approach: pcre.backtrack_limit defaults to 1,000,000 steps, past which preg_match returns false rather than a result — indistinguishable from "no match" unless you check preg_last_error().
A global regex remembers where it stopped
With g or y set, a RegExp object carries a mutable lastIndex property that both exec and test advance. The object is not a value; it is an iterator wearing the interface of a value, and that is the entire bug.
Concretely: with const re = /a/g, calling re.test("a") twice returns true and then false. The second call starts at index 1, finds nothing, resets lastIndex to 0, and so the third call is true again. A regex hoisted to module scope and shared across requests produces results that depend on how many times it has already been called — which is why the same pattern written inline inside a loop behaves differently from a hoisted constant.
The flag is load-bearing in three more places. String.prototype.match with g returns every match but discards capture groups entirely. matchAll keeps the groups but throws a TypeError if the regex lacks g. replaceAll throws a TypeError if handed a regex that has no g. None of these fail quietly, but none of them fail in the same direction.
Fix it by not sharing state: construct the regex where you use it, clone it with new RegExp(re.source, re.flags), or assign re.lastIndex = 0 before each run. The sticky flag y uses the same mechanism deliberately — it anchors every attempt at lastIndex exactly, which is what a tokeniser wants so that no input is silently skipped between tokens. The d flag, added in ES2022, populates match.indices with the start and end offset of every group, which removes most of the manual index arithmetic people write around exec.
Without the u flag, the dot is a UTF-16 code unit
JavaScript strings are UTF-16 and a plain regex matches code units, not characters. Anything above U+FFFF is stored as a surrogate pair, so /^.$/.test("😀") is false — the emoji is two units. Add the u flag and /^.$/u.test("😀") is true. A character class is worse: without u, [😀😁] is read as a set of four surrogate code units and can match half of an entirely unrelated emoji.
What the u flag does not do is make the shorthand classes Unicode-aware. \w is [A-Za-z0-9_] in every mode, permanently, so \w+ against café matches caf and stops. \d is [0-9] and never matches Arabic-Indic or Devanagari digits. \b is defined in terms of \w and inherits the same blind spot. The replacements are \p{L}, \p{N} and script properties like \p{Script=Devanagari}, all of which require u or v.
The u flag also changes case-insensitive matching, because it switches to Unicode simple case folding. /k/iu matches the Kelvin sign U+212A; /k/i does not. Whether that is a feature or a homoglyph vulnerability depends on whether you are searching text or comparing identifiers.
The v flag from ES2024 is a stricter superset. It adds set difference and intersection inside character classes — [\p{L}--[a-z]] is every letter except ASCII lowercase — and properties of strings such as \p{RGI_Emoji}, which is the only sane way to match a multi-code-point grapheme like a zero-width-joiner family emoji. Even so, a u-mode dot still matches exactly one code point, not one user-perceived character; for that use Intl.Segmenter rather than a pattern.
Where the same pattern gives a different answer
POSIX ERE, as used by grep -E and awk, uses leftmost-longest semantics: it returns the longest possible overall match. Perl-derived engines including JavaScript use leftmost-first, so an alternation returns the first branch that succeeds. Run a|ab against ab and POSIX gives you ab while JavaScript gives you a. Reordering the branches changes the JavaScript answer and leaves the POSIX one alone.
$ is not the same anchor everywhere either. In Python and PCRE it also matches immediately before a newline at the end of the string, so ^\d+$ happily accepts "123\n" — a genuine validation hole when the input arrived from a file or a request body. Python's \Z and PCRE's \z mean end-of-string with no exceptions. JavaScript's $ already behaves that way.
Lookbehind is unevenly supported and unevenly limited. JavaScript allows variable-length lookbehind such as (?<=\w+), which most engines do not: Java requires a bounded length and PCRE1 required a fixed one, so a pattern that runs in Node can fail to compile in the JVM. In the other direction, RE2-based engines have no lookaround whatsoever and Go's regexp rejects it at compile time. Escaping rules diverge too — POSIX BRE requires backslashes before (, ) and the brace quantifier, which is precisely the opposite of ERE.
The most expensive version of this lesson is usually email. There is no short, correct RFC 5322 pattern, and the WHATWG HTML specification says so explicitly: it defines a deliberately simplified pattern for <input type="email"> and describes it in the spec text as a "willful violation" of RFC 5322, because the real grammar allows comments, quoted strings and folding whitespace that nobody wants in a form field. Use a pattern to catch obvious typos, and confirm the address by sending mail to it.
Related tools
Try these tools that work well with this one
Cron Expression Generator
Convert natural language schedules to cron expressions
API Tester
Send HTTP requests and view responses
Base64 Decoder
Convert Base64 back to original text and files
Base64 Encoder
Convert text and files to Base64 format
CSV to JSON Converter
Convert CSV data to JSON
CSV to XML Converter
Convert CSV text to XML
Related guides
Tutorials and guides to get more from this tool
Mastering Regular Expressions: A Developer's Guide
Master regex in 7 minutes! Stop copying patterns blindly. Learn to write, test, and debug regular expressions like a pro in 2025.
URL Validation: Why Regular Expressions Aren't Enough
Validating URLs is harder than it looks. Explore why simple regex fails and how to ensure URLs are properly formatted and safe.
Regex Cheat Sheet: Patterns That Hold Up in Production
A practical regex reference with tested patterns for IDs, dates, URLs and logs, plus the mistakes that cause bugs and catastrophic backtracking.
ThenCatch is a free developer toolkit. Learn more about us.