Skip to content
Tresna

Regex Tester

Test a JavaScript regular expression against your own text. Every match with its offset and capture groups, a replace preview, and the browser's own error message.

pattern

4 matchesg3

matches

Every match, with its offset in the test text and the text each capture group held.
#atmatchgroups
11:102024-11-05
  • $1 year: 2024
  • $2 month: 11
  • $3 day: 05
21:302025-03-17
  • $1 year: 2025
  • $2 month: 03
  • $3 day: 17
32:52026-01-01
  • $1 year: 2026
  • $2 month: 01
  • $3 day: 01
42:271999-12-31
  • $1 year: 1999
  • $2 month: 12
  • $3 day: 31
43 named

Runs in this browser tab, using this browser’s own regular expression engine — the same one your code will use. Nothing is uploaded, stored or logged.

How to use this tool

A regular expression is easy to write and hard to read, and the two halves of that sentence are why there are regex testers at all. This one runs your pattern against your text and shows you every match, where it is, and what each capture group held. What it deliberately does not do is tell you the pattern is correct — it tells you what this browser's engine does with it, which is the part you can actually observe.

  1. 1Type the pattern between the slashes, without the slashes themselves, and tick the flags you want.
  2. 2Paste the text to test against. The match list appears immediately, with a line and column for each one.
  3. 3Open **Replace** to see what `String.replace` would produce, with `$1` and `$<name>` resolved by the engine rather than by this page.

It runs your browser's engine, and only that

There is no hand-written matcher here. The page hands your pattern to `new RegExp`, the same call your own code makes, and shows you the result. That is a deliberate choice with a specific consequence: what you see is true for JavaScript in this browser today, and it does not transfer.

A pattern that works here may fail in Python, in Go, in a database query, or in the same browser on an older version. The flavours genuinely differ — PCRE has possessive quantifiers and atomic groups, JavaScript did not get lookbehind until 2018, and Python's `re` has no `\p{...}` at all. If you are writing the pattern for something other than a browser, test it there.

Here is a real disagreement between two engines, on the same pattern and the same three characters. It is worth knowing because both answers are correct and neither is a bug:

// JavaScript
[..."aaa".matchAll(/a*?/g)].map(m => m[0])
// ["", "", "", ""]      four

// Python
re.findall(r"a*?", "aaa")
// ['', 'a', '', 'a', '', 'a', '']   seven
JavaScript finds four. Python finds seven.

The lazy quantifier can match nothing *or* one `a`, and the two engines disagree about whether both are reported at the same position. JavaScript advances past a position once an empty match has been taken; Python tries the longer alternative at the same offset first. A pattern with a lazy quantifier that can also match something real is therefore the case to be most careful about when moving between languages.

The error message is the same story. It is the browser's own text, shown verbatim, and the wording differs between Chromium, SpiderMonkey and JavaScriptCore. When your pattern is rejected, the message you will meet in your own console is the one on this page — which is precisely why it has not been tidied up.

This page adds the g flag, and says so

Without `g`, `RegExp.exec` and `String.match` return exactly one match — the first — and stop. That is correct behaviour, and it is almost never what someone typing into a tester wanted. So this page adds `g` when your pattern does not have it, and marks the status bar with `+g added` rather than quietly changing what you asked for.

const re = /\d+/;          // no g
"a1 b22".match(re)     // ["1"]      one match, and it stopped
"a1 b22".match(/\d+/g) // ["1", "22"]

// exec is the same: lastIndex is only consulted when g is present
const sticky = /\d/g
sticky.exec("a1 b22")  // "1"
sticky.exec("a1 b22")  // "22"
The difference, from the engine itself.

There is a `g` in JavaScript that is not this one: `y`, sticky, which anchors the match to `lastIndex` and gives up at the first miss. It is genuinely useful for tokenising, and it will find far fewer matches than you may expect. This page does not adjust it.

The empty match, which is real and which testers usually hide

A pattern like `a*` can match without consuming anything. The engine finds those, and so does this page — shown as `(empty match)` rather than as a blank cell, which would read as a bug.

[..."aaa".matchAll(/a*/g)].map(m => m[0])
// ["aaa", ""]     greedy: one match takes all three characters, then
//                  one empty match at the position just past the end

[..."aaa".matchAll(/a*?/g)].map(m => m[0])
// ["", "", "", ""]  lazy: an empty match at each of the four positions
Greedy, lazy, and the count that surprises people.

Four, not five and not three. A pattern that can match nothing is tried at every position, and a three-character string has four boundaries in it — before the first character, between each pair, and after the last. The greedy form visits two of those because its first match swallows the rest of the text.

If the list ever looks wrong in a way you did not expect, this is the first thing to check: change `*` to `*?` and see whether the count changes. A pattern that can match nothing is also a pattern that can match a very great deal.

A group that did not match is not a group that matched nothing

These two look identical in a table and are not the same thing. A group that took part in the match and captured an empty string is data. A group that did not take part at all — because an alternation went the other way, or an optional group was skipped — is reported here as `no match`, so you can tell which happened.

const re = /(\d)?x/g

re.exec("1xx")   // match "1x", group 1 is "1"
re.exec("1xx")   // match "x",  group 1 is undefined

// A round trip through String(matches) would render both as "" and lose
// the fact that the first one matched a digit and the second did not match
// anything at all.
Two matches of one pattern, and they differ.

That distinction matters most with alternations, where it tells you which branch the engine took, and with optional groups, where it tells you the value was genuinely absent rather than present and empty.

Named groups are numbered by when they open

A group containing another group is the outer one first. `(?<outer>(a))` makes `outer` group 1 and the inner group 2, even though the inner one finishes first. This page numbers them the way the engine does, and shows `$1` alongside the name so there is no ambiguity.

/(?=(a))(?<hit>a)/.exec("a")
// 1: "a"     the group inside the lookahead
// 2: "a"     the named group
// groups.hit is group 2, not group 1

/(?=(a))/.exec("a")   // 1: "a"  - the lookahead itself is not a group
A lookaround takes no number, but a group inside one does.

The one thing this page cannot protect you from

Some patterns take exponential time on some inputs. This is not a bug in your regex; it is how backtracking works. A greedy quantifier nested inside another one, with a failure at the end, makes the engine try every way of splitting the text before it gives up.

/^(a+)+$/.test("a".repeat(20) + "!")    // instant: fails immediately
This one is fine.

This one is not.

// ^(a+)+$ against "aaaaaaaaaaaaaaaaaaaa!"
// tries every way of dividing the a's between the inner and outer
// groups before it can conclude there is no match.
//
// 28 characters:  noticeable
// 32 characters:  seconds
// 36 characters:  minutes
This one hangs. Do not paste it into a page you need.

The fix, when a pattern needs to be rewritten, is usually to make the inner quantifier possessive in spirit: replace a nested `(a+)+` with a single `a+` and anchor the whole thing, or replace `.*` between two markers with `[^x]*` so the engine cannot backtrack across the excluded character at all.

The replace preview is the engine's, not this page's

The replacement is applied by `String.prototype.replace` with your own replacement string, so the preview is exactly what your code would produce. That matters because the rules for `$` are sharper than most people expect:

SequenceMeans
$1 … $9the text that group 1 to 9 captured
$<name>the text a named group captured
$&the whole match
$$one literal dollar sign
$10group 10 if the pattern has one, otherwise $1 followed by a 0

That last row is the one to remember. A pattern with nine groups and a replacement of `$10` does not fail and does not give you a dollar sign and a zero — it gives you the first group's text followed by a `0`. There is a test in this project's suite at both nine and ten groups, because it is exactly the sort of rule that is easy to reimplement slightly wrong.

And there is no escape for a literal dollar other than `$$`. A replacement of `$5.00` is not five dollars; it is group 5 followed by `.00`, or the literal text `$5.00` if there is no group 5.

Offsets count UTF-16 code units

JavaScript string indices are not character positions in the way a text editor counts them. An emoji outside the basic plane is two code units, so everything after it is shifted by two relative to what you would count by hand.

const s = "🎉x"
s.indexOf("x")        // 2, not 1
Array.from(s).length  // 2 - the display length, which is a different question
The same two facts, from the engine.

So the offset this page shows will sometimes disagree with the column your editor reports. Both are right; they are counting different things. The line and column beside each match is derived from the same offset, so it is consistent with the number and not with the editor.

Patterns that work and quietly do not do what you meant

  • An unescaped `.` matches any character, so `/api.v1/` also matches `apiXv1`. Write `/api\.v1/`.
  • A greedy `.*` between two markers runs to the *last* marker, not the first. `<!--.*-->` over two comments gives you one enormous match. Use `[^]*?` or the `s` flag with `.*?`.
  • `^` and `$` match the whole text, not each line, unless `m` is on. This is the single most common reason a pattern that works on one line finds nothing in a file.
  • `\b` is a word boundary defined by the `\w` class, so it treats an underscore as a word character. `snake_case_name` is one word to `\b`.
  • `\d` is ASCII 0–9 only, even in Unicode mode. Arabic-Indic digits need `\p{Nd}`, which needs the `u` flag.
const text = "<!--a--> keep <!--b--> keep"

text.match(/<!--.*-->/g)
// ["<!--a--> keep <!--b-->"]     one match, the last marker wins

text.match(/<!--[\s\S]*?-->/g)
// ["<!--a-->", "<!--b-->"]       two matches, as intended
A greedy match that swallows more than intended.

What the flags do

FlagEffect
gFind every match instead of the first.
iIgnore letter case.
m^ and $ match each line rather than the whole text.
sLet . match a newline.
uUnicode mode. Required for \p{...} property escapes.
ySticky. Anchor to lastIndex and stop at the first miss.
dRecord match indices on the result. No effect on what matches.
vUnicode sets mode. Stricter than u, and cannot be combined with it.

`u` and `v` are mutually exclusive and the engine rejects both together. This page swaps one for the other when you tick the second, because a syntax error for a thing the buttons could have made impossible is a poor trade.

Doing it yourself

The built-in is enough. There is no need for a regex library to test a regex, and if you find yourself reaching for one in application code, the more useful question is usually whether the job wants a parser.

re.exec(text)          // first match, or null
re.lastIndex           // where the last one stopped - only moves with g or y
text.match(re)         // array of the full matches, or null
text.matchAll(re)      // iterator of match objects; requires g
text.replace(re, "$1") // substitution, with the engine's $ rules

// Match objects carry more than the text:
const m = /(?<year>\d{4})/.exec("in 2026")
m.index      // 3     where it starts
m[0]         // "2026"  the whole match
m.groups     // { year: "2026" }
m.input      // the whole text it was found in
The five things worth memorising.

Frequently asked questions

Why does it say g was added?
Because without it your pattern would return only the first match, which is what the engine does and almost never what someone testing a pattern wanted. The page adds it and marks the status bar rather than quietly changing your pattern, so the match list is never a surprise.
Will a pattern that works here work in Python or Go?
Not necessarily, and this page cannot tell you. It runs JavaScript's engine, in this browser. Python's re has no \p{...}, PCRE has features JavaScript does not, and lookbehind only reached JavaScript in 2018. Test the pattern in whatever it is actually for.
Is this safe to use on untrusted text?
No regex tester can make that safe, and this one says so rather than implying otherwise. A pattern with a nested quantifier can take exponential time on the wrong input, and JavaScript offers no way to interrupt a running regular expression — there is no timeout a page can impose. The input and match limits here bound the damage; they do not remove the risk. Use an environment you can kill.
What does no match mean for a group?
That the group did not take part in this match at all, which is different from a group that matched an empty string. It usually means an alternation went the other way or an optional group was skipped, and telling them apart is how you work out which branch the engine took.
Why is the offset different from my editor's column?
JavaScript counts string indices in UTF-16 code units, so an emoji counts as two and everything after it shifts. The line and column shown here are derived from the same offset, so they agree with it and disagree with an editor that counts characters. Neither is wrong.
Why does my replacement not contain a dollar sign?
Because $ starts a substitution. $$ is how you write a literal one. $5.00 is group 5 followed by .00, not five dollars, and $10 is group 10 if the pattern has ten groups or $1 followed by a 0 if it has nine.
Does this send my pattern anywhere?
No. It runs in your browser tab, against your own browser's engine. Nothing is uploaded, stored or logged — which matters more here than the privacy line usually does, because a pattern often names an internal host or a path you would rather keep to yourself.