Skip to main content

Regex Tester

Test regular expressions safely. Matching runs in a Web Worker with a hard timeout, so a catastrophic-backtracking pattern cannot freeze your browser tab.

Your data never leaves the browserUpdated: July 2026

Write a pattern or load the sample to begin.

Key Takeaways

  • Matching runs inside a Web Worker under a hard timeout, so a catastrophically backtracking pattern cannot lock up your tab.
  • A RegExp carrying the g flag keeps a mutable lastIndex, which is why calling test repeatedly on the same object alternates true and false.
  • Zero-length matches do not advance the exec loop; without bumping lastIndex by hand you get an infinite loop.
  • The \w shorthand is ASCII-only, and the Turkish dotted/dotless i pair breaks naive case-insensitive matching.

Catastrophic backtracking and why a worker is used

The JavaScript regular expression engine is a backtracking engine: when a path fails it rewinds and tries the alternatives. Nested quantifiers make the number of those alternatives grow exponentially with input length. The pattern /^(a+)+$/ is the textbook case. Give it a subject made entirely of a characters with one trailing character that defeats the match, and the engine has to try every possible way of splitting those a characters into groups.

javascript
const pattern = /^(a+)+$/; pattern.test('aaaaaaaaaa!');   // 10 a's: instantpattern.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // 30 a's: minutes // Why: (a+)+ tries 2^(n-1) ways of splitting n a's into groups.// Because the final character never matches, the engine must exhaust ALL of them. // Fix 1: remove the nested quantifier/^a+$/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // instantly false // Fix 2: remove the ambiguity/^(?:[^a]|a)+$/ // non-overlapping alternatives leave nothing to backtrack into

This is the attack surface known as ReDoS. If you match user-supplied text against such a pattern on a server, an attacker can occupy a process for minutes with a single request. In a browser the effect is more visible: the tab freezes, because matching runs synchronously on the main thread and nothing can interrupt it.

Note

This tool runs matching inside a dedicated Web Worker and arms the timer on the main thread. If the budget is exceeded the worker is terminated and you get a catastrophic-backtracking warning. That means you can deliberately try a dangerous pattern and observe the result without losing the page.

The lastIndex state and alternating results

In JavaScript a regular expression is a stateful object, not an immutable value. With the g or y flag it carries a writable lastIndex field, and both test and exec advance it after every successful match. Reuse the same object across calls and results alternate between true and false. This is one of the most common regular expression bugs in production code.

javascript
const re = /ada/g; re.test('ada'); // true   (lastIndex is now 3)re.test('ada'); // false  (search started at index 3, lastIndex resets to 0)re.test('ada'); // true   (the cycle begins again) // A module-level pattern is dangerous for exactly this reason:const EMAIL = /\S+@\S+/g;function isValid(value) {  return EMAIL.test(value); // false on every second call with the same input} // Three correct approaches:const EMAIL_STATELESS = /\S+@\S+/;      // 1. drop the g flagEMAIL.lastIndex = 0;                      // 2. reset before each use[...value.matchAll(/\S+@\S+/g)];         // 3. use matchAll (it works on a clone)

Zero-length matches and infinite loops

When a pattern can match the empty string (a*, \b, (?:) and friends), an exec loop does not advance on its own. A zero-length match leaves lastIndex where it was, so the loop reads the same position forever. This is precisely why the matcher behind this tool uses a hand-written exec loop rather than matchAll: it bumps lastIndex manually whenever it sees a zero-length match.

javascript
const re = /a*/g;let m; // INFINITE LOOP: an empty match at every position in 'bbb', no progresswhile ((m = re.exec('bbb')) !== null) {  console.log(m.index, JSON.stringify(m[0])); // 0 "" ... 0 "" ... 0 ""} // CORRECT: advance the cursor yourself on a zero-length matchwhile ((m = re.exec('bbb')) !== null) {  console.log(m.index, JSON.stringify(m[0]));  if (m[0].length === 0) {    re.lastIndex++;    if (re.lastIndex > 'bbb'.length) break;  }}

Non-ASCII letters and the Turkish i

The \w shorthand is defined over ASCII and means exactly [A-Za-z0-9_]. None of ş, ğ, ı, ü, ö or ç match it. A \w+ pattern trying to capture words in Turkish text splits "çiçek" into "i" and "ek". The same limitation applies to the \b word boundary, since it is defined in terms of \w.

javascript
'çiçek bahçesi'.match(/\w+/g);// ['i', 'ek', 'bah', 'esi']   <-- the Turkish letters counted as boundaries // Fix 1: spell the character class out'çiçek bahçesi'.match(/[a-zçğıöşü]+/gi);// ['çiçek', 'bahçesi'] // Fix 2: the u flag with a Unicode property escape (cleanest)'çiçek bahçesi'.match(/\p{Letter}+/gu);// ['çiçek', 'bahçesi']

The subtler problem is case folding. Turkish treats the dotted and dotless i as separate letters: the uppercase of i is İ, and the uppercase of ı is I. The regular expression engine, however, uses the default Unicode case-folding table, in which the uppercase of i is I. As a result the i flag produces wrong matches for Turkish text, and because the bug only appears in words containing an i it slips through test suites easily.

PatternSubjectResultReason
/İSTANBUL/iistanbulno matchUnicode folds i to I, never to İ
/ISTANBUL/iıstanbulno matchDotless ı is not folded to I
/ISTANBUL/iistanbulmatchASCII folding happens to do the right thing
/[İIiı]stanbul/İstanbulmatchSpelling all four variants out is the safe route
For case-insensitive search over Turkish text, do not rely on the i flag; write the character class explicitly.

In application code the most reliable approach is normalising both sides with toLocaleLowerCase("tr") before comparing. In user-facing places such as a search box, that single line removes every surprise the i flag can produce. The same class of problem exists in other locales too, so if you localise, treat case-insensitive matching as a locale-dependent operation rather than a free one.

Named groups and the d flag

Naming capture groups instead of indexing them removes the match[3] style access that breaks the moment you edit the pattern. The d flag adds an indices array giving the start and end offsets of every group, which is what you need for highlighting or partial replacement. This tool shows named groups as their own column in the results table.

javascript
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/d;const m = pattern.exec('due: 2026-07-26'); m.groups.year;         // '2026'm.groups.month;        // '07'm.indices.groups.day;  // [13, 15]  <-- undefined without the d flag // Replacement can address groups by name too:'2026-07-26'.replace(pattern, '$<day>/$<month>/$<year>');// '26/07/2026'

Frequently Asked Questions

Is the text I enter sent to a server?
No. Your pattern and subject never leave the browser. Matching happens in a Web Worker shipped with the page and involves no network request, so you can work comfortably with real log lines or production data.
I ran a pattern and got a timeout warning. What now?
Your pattern contains a nested quantifier; look for shapes such as (a+)+ or (\d+)*. The usual fix is to make the inner group unambiguous or stop the alternatives from overlapping. The same pattern will behave identically on a server, where nothing protects you the way the browser sandbox does here.
Which flags are supported?
Every flag the JavaScript engine accepts: d (match indices), g (global), i (case-insensitive), m (multiline), s (dot matches newlines), u and v (Unicode modes) and y (sticky). The u and v flags are mutually exclusive, because the engine treats using both as a syntax error.
Why do I see a limited number of matches instead of hundreds of thousands?
The result list is capped deliberately. A near-empty pattern can match at every position of a large subject, and rendering that many results would freeze the interface even though the regular expression itself finished instantly. When the cap is reached, a truncation notice is shown.
Will a pattern from here behave the same in Python or PHP?
The core syntax is largely shared, but differences exist. Python requires fixed-width lookbehind, PCRE offers atomic groups and possessive quantifiers that JavaScript lacks, and Unicode property escapes are spelled differently across languages. This tool uses the JavaScript engine, so its results are exact for browser and Node environments.