Every regex flavour is slightly different, and the differences are exactly where bugs live. This reference covers JavaScript's RegExp engine specifically — what V8, SpiderMonkey and JavaScriptCore actually implement, not the Perl or Python behaviour you may be remembering. Paste anything here into the Regex Tester to watch it run against real input.
Character Classes
| Pattern | Matches |
|---|---|
\d |
An ASCII digit, exactly [0-9]. It never matches Arabic-Indic or fullwidth digits — use \p{Nd} with the u flag for those. |
\D |
Any character that is not [0-9] |
\w |
A word character: [A-Za-z0-9_]. ASCII only, even with the u flag. |
\W |
Anything outside [A-Za-z0-9_] |
\s |
Whitespace: space, tab, CR, LF, vertical tab, form feed, plus the no-break space U+00A0, the Unicode space separators, the line and paragraph separators U+2028 and U+2029, and the byte order mark U+FEFF |
\S |
Any non-whitespace character |
. |
Any character except the line terminators \n, \r, U+2028 and U+2029 — unless the s flag is set |
[abc] |
Any one of a, b, c |
[^abc] |
Any single character that is not a, b or c |
[a-z0-9_] |
Ranges, by code point. Combine as many as you need. |
[\d\s] |
Shorthand classes nest inside brackets — "a digit or a whitespace character" |
\p{L}, \p{Script=Greek} |
Unicode property escapes. Require the u or v flag. |
Inside a character class, only ^ (when first), ], \ and - need escaping. . and + are literal there, so [.+] is fine.
Anchors and Boundaries
| Pattern | Matches |
|---|---|
^ |
Start of the input — or the start of any line when the m flag is set |
$ |
End of the input — or the end of any line with m. Unlike Perl, JavaScript's $ does not also match before a trailing newline. |
\b |
A word boundary: the zero-width position between a \w and a non-\w. Because \w is ASCII, naïve contains a boundary right before the ï. |
\B |
Any position that is not a word boundary |
Quantifiers
| Pattern | Repeats the previous item |
|---|---|
* |
Zero or more times |
+ |
One or more times |
? |
Zero or one time (makes it optional) |
{n} |
Exactly n times |
{n,} |
n or more times |
{n,m} |
Between n and m times, inclusive |
*? +? ?? {n,}? {n,m}? |
Lazy variants — match as few characters as possible |
Quantifiers are greedy by default. On <b>hi</b>, the pattern <.+> swallows the whole string; <.+?> stops at the first > and returns <b>. JavaScript has no possessive quantifiers (*+) and no atomic groups ((?>…)), so lazy matching and precise classes are your only tuning knobs.
Groups and Alternation
| Pattern | Meaning |
|---|---|
(abc) |
Capturing group. Numbered from 1, left to right, by opening parenthesis. |
(?:abc) |
Non-capturing group — grouping for a quantifier or alternation without spending a capture slot |
(?<year>\d{4}) |
Named capturing group, read back as match.groups.year |
\1 |
Backreference to whatever group 1 captured |
\k<year> |
Backreference to a named group |
a|b |
Alternation, with the lowest precedence of any operator. ^cat|dog$ means "starts with cat" OR "ends with dog" — write ^(?:cat|dog)$ for the obvious reading. |
$1, $<year> |
Refer to a group inside a .replace() replacement string |
Lookaround
All four assertions are supported in modern JavaScript; lookbehind landed in ES2018 and is available in every current browser.
| Pattern | Asserts |
|---|---|
(?=abc) |
Positive lookahead — what follows is abc |
(?!abc) |
Negative lookahead — what follows is not abc |
(?<=abc) |
Positive lookbehind — what precedes is abc |
(?<!abc) |
Negative lookbehind — what precedes is not abc |
Lookaround is zero-width: it tests a position without consuming characters, so the assertion never appears in the match. JavaScript is unusually generous here — it permits variable-length lookbehind such as (?<=\$\d+), which Python's re and Java both reject.
Escapes
| Pattern | Produces |
|---|---|
\. \* \+ \? \( \) \[ \] \{ \} \^ \$ \\ |
The literal metacharacter. A backslash before the alternation bar escapes that too. |
\/ |
A literal slash — needed only inside a /…/ literal, not in new RegExp() |
\n \r \t \f \v \0 |
Newline, carriage return, tab, form feed, vertical tab, NUL |
\xA9 |
A character by two-digit hex code — here the copyright sign |
\u00e9 |
A character by four-digit hex code — here a lowercase e-acute |
\u{1F600} |
A code point above U+FFFF. Requires the u or v flag. |
When you build a pattern from user input with new RegExp(str), remember the string is parsed twice — once by the JavaScript lexer, once by the regex engine — so a literal backslash needs \\\\ in a string literal. Our string escape tool handles that layering if you are hand-assembling patterns.
Flags
| Flag | Effect |
|---|---|
g |
Global. .replace() replaces every match, .matchAll() requires it, and .exec()/.test() resume from the regex's lastIndex — which is why calling .test() repeatedly with g alternates true and false. |
i |
Case-insensitive matching |
m |
Multiline. ^ and $ match at every line break rather than only at the string's ends. It does not change what . matches. |
s |
dotAll. . now also matches line terminators. |
u |
Unicode mode. The pattern is read as code points, \u{…} and \p{…} become available, and unrecognised escapes throw instead of being silently accepted. |
y |
Sticky. The match must begin exactly at lastIndex; the engine will not scan forward looking for one. |
Two more you will meet: d (ES2022) adds a .indices array with the start and end offset of every group, and v (ES2024) is a strict superset of u that adds set operations inside character classes, like [\p{L}--[a-z]].
Worked Patterns
Loose email check — /^[^\s@]+@[^\s@]+\.[^\s@]+$/
Matches ada@example.com, rejects ada@example. Deliberately loose: a fully RFC 5322-compliant email regex runs to thousands of characters and still rejects valid addresses. Use a cheap shape check like this, then confirm the address by sending mail to it.
URL in free text — /https?:\/\/[^\s/$.?#][^\s]*/gi
Pulls https://thetextsolutions.com/regex-tester/ out of a paragraph. For anything beyond extraction — reading the host or query string — parse with new URL() instead.
CSS hex colour — /#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})\b/gi
Matches #fff, #2980B9 and the 8-digit #2980B9CC. Two things are doing the work. Alternation is first-match-wins, so the longest branch is listed first; drop the trailing \b and reverse the order and #2980B9 matches only #298. The \b is the safety net — it rejects a short branch that stops mid-code and forces the engine to backtrack into a longer one.
ISO 8601 date — /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/
Matches 2026-08-02 and rejects 2026-19-45. The simpler ^\d{4}-\d{2}-\d{2}$ accepts month 19 quite happily. Neither version knows February has 28 days — no regex should try.
Collapse runs of whitespace — /\s+/g replaced with a single space
Turns "too many\n\nspaces" into "too many spaces". Pair it with .trim(). The text cleaner does this without any regex at all.
Reformat a date with capture groups — /(\d{4})-(\d{2})-(\d{2})/g replaced with $3/$2/$1
Rewrites 2026-08-02 as 02/08/2026. The named form /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/ with $<d>/$<m>/$<y> survives someone adding a group later.
Doubled words — /\b(\w+)\s+\1\b/gi
The backreference finds the the in "paste the the pattern". The i flag catches The the too.
Thousands separators — /\B(?=(?:\d{3})+(?!\d))/g replaced with ,
Turns 1234567 into 1,234,567. It matches only empty positions that have a whole number of digit-triples ahead of them, and \B stops it firing before the first digit.
Catastrophic Backtracking
A pattern like /^(a+)+$/ is a trap. Against aaaaaaaaaaaaaaaaaaaaaaaaaX, the inner a+ and the outer + can divide the same run of as in exponentially many ways, and because the trailing X guarantees failure the engine tries every single one before giving up. Twenty-five characters is enough to hang a tab.
The warning sign is a quantifier applied to a group that already contains a quantifier matching the same characters — (a+)+, (\w*\s*)+, (.*,)*. The fix is to remove the ambiguity so only one split is possible: ^a+$ here, or a negated class like [^,]*, in place of .*,. Since JavaScript offers no atomic groups, you cannot simply tell the engine to stop backtracking.
Our Regex Tester runs your pattern inside a Web Worker with a hard timeout, so a runaway pattern is killed rather than freezing the page. That makes it a safe place to deliberately try (a+)+$ on a long non-matching string and see the failure for yourself — a lot more instructive than reading about it.
Frequently Asked Questions
Does JavaScript support lookbehind?
Yes. (?<=…) and (?<!…) are part of ES2018 and work in all current browsers and Node. JavaScript also allows variable-length lookbehind, which most other engines do not.
Why does \d not match Arabic or Devanagari digits?
In JavaScript \d is defined as exactly [0-9], with no Unicode-aware mode. Use \p{Nd} together with the u flag when you need every decimal digit.
Why does my .test() return true, then false, then true?
The g flag makes the regex stateful. Each .test() starts from lastIndex and updates it, so results alternate. Drop the g flag for boolean checks, or reset re.lastIndex = 0.
Should I use a regex to validate email addresses? Only as a rough shape check. The authoritative test is delivering a message to the address; a complex regex mostly adds false rejections.
Build your pattern a piece at a time in the Regex Tester and check the matches after every change — it is far faster than debugging a finished forty-character expression.