Developer Utilities

Regex Tester

Test and debug regular expressions with multiple flags.

Regular Expression
//
✨ AI Generate (Cloudflare Workers AI)
Test Text
Common Templates

What is Regex Tester?

Regular expressions (regex) are patterns used for matching text. The regex tester helps you write, test, and debug regular expressions with real-time feedback.

Features

  • Real-time Matching: See matches as you type
  • Flags Support: g (global), i (case-insensitive), m (multiline)
  • Match Highlighting: Visually see matched text
  • Group Capture: View captured groups

Common Patterns

  • Email: /^[\w.-]+@[\w.-]+\.\w+$/
  • URL: /https?:\/\/[\w.-]+/
  • Phone: /\d{3}-\d{4}-\d{4}/

Tests ECMAScript regular expressions against pasted text, listing every match with its index and numbered capture groups. The pattern string and six flags (g, i, m, s, u, y) go directly to the RegExp constructor, so results equal what Node.js and Chrome produce; that matters when a pattern copied from PCRE, Python or grep behaves differently in JavaScript.

What this engine refuses to compile

The pattern string is handed unescaped to new RegExp(), so ECMA-262 alone decides what compiles. PCRE syntax simply throws: a++ returns "Nothing to repeat" and (?>a) returns "Invalid group", because JavaScript has no possessive quantifiers and no atomic groups. \A, \z and \G do not exist either; anchoring is ^, $ or the y flag. Lookbehind is available, so (?<=\$)\d+ finds 42 inside "$42 and 7". Unicode property escapes also work, but only with the u flag: without it \p{Han} is read as a literal p followed by braces. An error in this panel therefore usually means the pattern was ported from another flavor, not mistyped.

Each flag changes meaning, not just speed

The six checkboxes map to g, i, m, s, u and y; g starts enabled, which is why the list shows all matches instead of the first. m redefines ^ and $ at the four ECMAScript line terminators (LF, CR, U+2028, U+2029) and does nothing else: . still refuses to cross them unless s is also on. i without u applies simple case folding, so /k/i does not match the Kelvin sign U+212A and /s/i does not match U+017F; switching u on makes both match. y anchors every attempt at lastIndex, so with y but no g only a match starting at position 0 gets reported. u also makes . consume one code point: /^.$/u matches a surrogate pair, /^.$/ does not.

Offsets are UTF-16 units, empty matches are stepped

The number beside a match is match.index, an offset in UTF-16 code units rather than characters. A CJK character costs one unit, an emoji costs two, so a four-byte emoji early in a line shifts every later index by two relative to what your editor calls a column. Empty matches are reported as real results too: \b with g over "hi there" yields positions 0, 2, 3 and 8. After each zero-length hit the loop increments lastIndex by hand, because exec would otherwise return the same empty match forever. Groups come from match.slice(1): numbered by opening parenthesis, rendered blank when the group did not participate, and named groups appear in that same numeric order instead of by name.

Worked example

Global matches report UTF-16 offsets

Input
/(\d+) 元/g on 咖啡 12 元, 茶 8 元 😀
Output
"12 元" at index 3 (group 1 = "12"); "8 元" at index 11 (group 1 = "8")

Each CJK character is one UTF-16 unit, so two characters plus a space put the first match at offset 3, and the comma and space before 茶 push the second to 11. The trailing emoji costs two units, which is why the string measures 17 units but only 16 code points. Unchecking g leaves exactly the first match in the list.

Limits and pitfalls

  • Only six flags are offered: the newer d (match indices) and v (unicode sets) flags cannot be tested, so set operations such as intersection are out of reach here.
  • No timeout and no input-size cap exist, and the match loop re-runs on every keystroke; a nested quantifier like (a+)+$ against 24 characters plus one non-matching symbol already blocks the tab for several hundred milliseconds, and each extra character doubles it.
  • The highlight preview renders the test text through v-html, so pasting text that contains <b> shows markup instead of the literal characters you typed.
  • Loading a template overwrites only the three flags it declares (g, i, m); s, u and y keep whatever you set before, so a leftover y can silently turn a working pattern into "No matches found".
  • Unchecking g reports the first match only and the counter shows 1, even when the pattern would match hundreds of times further down the text.

Related tools

This page, another engine, or a script: which one settles it

The question in front of youRegex tester on this pageWhat to reach for instead
The pattern will run in Python, PHP or grep, not in JavaScriptOnly the engine of the browser you are sitting in compiles the pattern. (?P<year>\d{4}) fails here with "Invalid group" because ECMA-262 spells a named group (?<year>\d{4}); the pattern is not wrong where it will actually run.Test in the runtime that ships it — python3 -c "import re; re.compile(r'...')", or the PHP binary — or open regex101 and pick the PCRE2/Python flavour there. A passing verdict on this page says nothing about those engines.
You need the result of the replacement, not the list of matchesThere is no replacement field. The panel returns matches with index and numbered groups, which answers "is the pattern right" and never "what does the output look like"; building the new string stays manual.Run it in the same engine so the semantics cannot drift: text.replaceAll(/\d+/g, "#") in the browser console or in Node. Same RegExp implementation, and what prints is the string you wanted.
The text to search is spread across a directory of filesOne textarea, one paste, no file input. Each new paste overwrites the last, so four hundred files means four hundred copy-paste rounds and no record of which file a hit came from.rg -n "pattern" src/ prints file:line per hit and reads the files itself. If the pattern uses lookbehind, add -P: ripgrep's default engine rejects lookaround, its PCRE2 mode accepts it.
Carrying a pattern that just worked into code or a ticketThe copy button returns the pattern body only. The six checkboxes are separate state and travel nowhere, so whoever receives the pattern cannot tell whether i or m was on when the matches looked correct.Hand over the literal form your source will contain, /\d{2,4}/gi, flags included, or write the flag string next to the pattern in the ticket. A bare body is a different expression from the one you verified.
The pattern has to survive the next refactorNothing is kept: a reload clears the pattern, the flags and the test text, so the check you just performed cannot be repeated by a reviewer or by yourself on Friday.Move the samples into a fixture — three strings that must match, two that must not — and assert them with node --test from node:assert. This page is where a pattern is found; the fixture is what keeps it from rotting.

One concrete scenario

Thirty log lines and a counter that reads zero

Thirty lines of an nginx access log go into the test textarea, and ^([0-9.]+) .* ([0-9]{3}) ([0-9]+)$ goes into the pattern box with the flags left at their default, g alone. The result panel answers "no matches found" — zero, not one: with m off, ^ and $ bind only to the two ends of the whole paste, and . refuses to cross any of the 29 line feeds between them. Tick m and the counter reads 30, one row per line, three groups each: client IP, status, bytes. Then rewrite the status group as [45][0-9]{2} and exactly 14 of the 30 rows remain. Fourteen is the number to report; the fix was the flag, not a different tool.

Frequently Asked Questions

Which regex flavor does this tester use?
JavaScript (ECMAScript) regex — exactly what runs in Node.js and browsers. Modern engines support constructs like lookbehind; syntax from other flavors (for example \p{...} without the u flag) may behave differently.
Why does .* match more than I expected?
Quantifiers are greedy by default: .* consumes as much as possible, then backtracks. Use the lazy form .*?, or a negated character class such as [^"]*, to stop at the first delimiter.
Can a regex be dangerous to run in production?
Yes. Nested quantifiers such as (a+)+$ can trigger catastrophic backtracking (ReDoS) on crafted input. Test with long adversarial samples and anchor your patterns where possible.

Content reviewed: