How to read a pattern
A regex describes text using symbols. The engine reads the pattern left to right and tries to fit it against the text, position by position. In JavaScript the pattern becomes a RegExp object applied over the string, that is exactly what the regex builder does under the hood, highlighting each stretch that matched. Reading gets easy once you sort the symbols by their job:
- Literals
- Ordinary characters (a, 7, -) match themselves.
- Anchors
- Pin a position without consuming text: ^ $ \b.
- Classes
- Say which characters fit at a position: [a-z], \d, .
- Quantifiers
- Repeat the previous item: * + ? {n,m}.
- Groups and alternation
- Bundle pieces and offer choices: (…) and |.
- Assertions
- Look around without consuming: lookahead (?=…) and lookbehind (?<=…).
Anchors: where, not what
Anchors are the number-one source of confusion because they match no character: they assert a position. ^ requires the start of the text; $ requires the end. \b is a word boundary, the edge between a word character (\w) and a non-word one, handy so "cat" is not captured inside "category". \B is the opposite: a position that is not a word boundary. Because anchors consume no text, they are the cheapest antidote to backtracking: a $ at the end of a pattern tells the engine "either close here or give up", cutting short the tour through combinations it would otherwise take.
^abc → casa "abc" apenas no início
abc$ → casa "abc" apenas no fim
\bfoo\b → casa "foo" isolada, não "food" nem "barfoo"Character classes and Unicode
A class in square brackets matches exactly one character from the set. [abc] matches a, b or c; [a-z] uses a range; [A-Za-z0-9] mixes several. A ^ right after the bracket negates the class: [^0-9] matches anything that is not a digit. Inside brackets almost every metacharacter loses its power, a dot there is just a dot. The most common sets have shorthands, and each has an uppercase negation:
| Shorthand | Equivalent to | Negation |
|---|---|---|
| \d | [0-9] (digit) | \D (non-digit) |
| \w | [A-Za-z0-9_] (word, ASCII only) | \W (non-word) |
| \s | space, tab, newline | \S (non-space) |
| . | any character except a line break | (use the s flag to include \n) |
| \p{L} | any Unicode letter (needs the u flag) | \P{L} (non-letter) |
Look at the second and last rows: \w is ASCII only. It equals [A-Za-z0-9_] and excludes accents, "coração", "José" and "ñ" fall outside it. This is the silent mistake number one in non-English text. The modern fix is the u flag plus property escapes: \p{L} matches any Unicode letter, \p{N} any number, \p{P} punctuation. It is the same reason the character counter uses \p{L} instead of \w to count words. The \p{…} property escapes and the u flag have been standard ECMAScript since ES2018.
// Contar "palavras" em português
"São Paulo".match(/\w+/g) → ["S", "o", "Paulo"] // \w quebra no "ã"!
"São Paulo".match(/\p{L}+/gu) → ["São", "Paulo"] // \p{L} + flag u acertaWhy \w ignores ç, é and ã
\w was defined in the 1980s as an ASCII shorthand: exactly [A-Za-z0-9_]. It never knew what an accent is. You have two honest ways out: list the letters by hand, like [A-Za-zÀ-ÿ], which covers Latin-1 but forgets other scripts; or turn on the u flag and use \p{L}, which hands the "is this a letter?" question to the Unicode database. The second is the right one for real text, and the only one that also gets Greek, Cyrillic and every letter-like symbol that shows up.
Code point vs grapheme: why /./u miscounts emoji
Without the u flag the dot matches one UTF-16 code unit, not a character. Since almost every emoji lives above U+FFFF, it is stored in two code units (a surrogate pair), so "😀".length is 2 and /./g finds two matches where you see one. With the u flag the dot matches a full code point: /./gu finds a single match in "😀". But that is where the next gotcha hides.
A grapheme, what the eye calls "one character", can be several code points. The flag "🇧🇷" is two regional indicators (two code points, four code units); "👍🏽" is the thumb plus a skin-tone modifier. Against "🇧🇷", /./gu returns 2, not 1, because it counts code points. To count real graphemes, the ruler a human would use, the right tool is not regex but JavaScript’s own Intl.Segmenter, which groups code points into grapheme clusters.
The v flag (unicodeSets): the 2024 upgrade
ECMAScript 2024 added the v flag, a superset of u. It turns on set notation inside classes: intersection with &&, difference with --, and nested classes. For instance, [\p{Script=Greek}&&\p{Letter}] matches only Greek letters. Rules: u and v are mutually exclusive (using both throws a SyntaxError), and the v flag has shipped in current browsers since 2023. The regex builder on this page exposes u but not v, for most everyday patterns, u is already enough.
Quantifiers: greedy, lazy and possessive
A quantifier repeats the item immediately before it. By default they are greedy: they grab as much text as possible and only give some back (backtrack) if the rest of the pattern fails to fit. Adding ? makes the quantifier lazy, it grabs the minimum and only grows if it has to. There is a third mode, possessive, which grabs the maximum and never gives it back: the most direct defense against backtracking. The catch for JavaScript users is that the language has neither possessives nor atomic groups, but they can be emulated, and we will get there.
| Form | Behavior | In JavaScript? |
|---|---|---|
| .* .+ (greedy) | Grabs the maximum; backtracks if the rest fails. | Yes |
| .*? .+? (lazy) | Grabs the minimum; grows only if needed. | Yes |
| .*+ .++ (possessive) | Grabs the maximum and never backtracks. | No |
| (?>…) (atomic group) | Freezes what matched; drops the backtrack points. | No |
| (?=(…))\1 (emulated atomic) | Standard trick to simulate an atomic group in JS. | Yes (idiom) |
The classic case almost everyone gets wrong is pulling out HTML tags. The dot matches anything, so a greedy <.*> swallows too much. Compare the three patterns over the same text:
Texto: <b>negrito</b>
<.*> (guloso) → casa "<b>negrito</b>" , uma ocorrência, engole tudo
<.*?> (preguiçoso) → casa "<b>" e "</b>" , duas ocorrências, para no 1º >
<[^>]*> (classe certa) → casa "<b>" e "</b>" , igual, e sem risco de backtrackingThe lesson is worth gold: replacing .* with a negated class like [^>] solves the same problem the lazy quantifier would solve, but without leaving the engine backtrack points to explore. Whenever the delimiter is known (a >, a quote, a comma), prefer [^delimiter]* over .*?. You gain clarity and armor the pattern against the backtracking blow-up of the last section.
First worked example, now numeric extraction: \d+(?:[.,]\d+)? applied to "Total: R$ 123.45 (split into 3x of 41.15)" with the g flag returns three results, 123.45, 3 and 41.15. The \d+ grabs the integer part; the optional group (?:[.,]\d+)? captures the decimal part when it exists (which is why the "3", with no decimals, also matches). That is one of the ready-made patterns in the regex builder’s library.
Groups, flags, lookahead and lookbehind
Parentheses group a stretch so you can apply a quantifier to it or capture the matched value. (\d{4}) creates a capturing group, the engine stores what matched so you can reuse it (for instance, in a replacement with $1). If you only want to group, without storing, use a non-capturing group (?:…), which is lighter. Named groups (?<year>\d{4}) name the value, and you retrieve it in a replacement as $<year>. The pipe | is alternation: cat|dog matches either word. Named groups, like lookbehind, have been standard JavaScript since ES2018.
// Segundo exemplo trabalhado: reformatar data com grupos nomeados
"05/05/2026".replace(
/(?<dia>\d{2})\/(?<mes>\d{2})\/(?<ano>\d{4})/,
"$<ano>-$<mes>-$<dia>"
)
→ "2026-05-05"Lookahead and lookbehind are assertions: they inspect what comes before or after the current position without consuming that text. There are four, and the table below is the reference you will keep coming back to. The most common use is to validate without capturing, for example, requiring a password to have at least one digit with (?=.*\d), without that piece becoming part of the match.
| Syntax | Name | Asserts the position is… |
|---|---|---|
| (?=…) | Positive lookahead | …followed by … |
| (?!…) | Negative lookahead | …not followed by … |
| (?<=…) | Positive lookbehind | …preceded by … |
| (?<!…) | Negative lookbehind | …not preceded by … |
// Extrair só o número depois de "R$", sem levar o "R$" junto
"R$ 123,45".match(/(?<=R\$\s?)\d+(?:[.,]\d+)?/) → "123,45"
// Casar "gol" só quando NÃO for seguido de "eiro" (evita "goleiro")
/gol(?!eiro)/ → casa "gol" em "gol de placa", não em "goleiro"
// Separador de milhar: um dígito seguido de grupos de três até a borda
"1234567".replace(/\d(?=(\d{3})+\b)/g, "$&.") → "1.234.567"- ES2015The u flag (Unicode mode)
ECMAScript 2015 (ES6) brings Unicode mode: the dot and classes start reasoning per code point, and the road to \p{…} opens up.
- ES2018Lookbehind, named groups, \p{…}, dotAll
The big harvest: lookbehind (?<=) and (?<!), named groups (?<name>…), \p{…} property escapes and the s (dotAll) flag all land at once.
- ES2022The d flag (match indices)
The d flag (hasIndices) exposes the start and end positions of each matched group, handy for highlighting stretches of text.
- ES2024The v flag (unicodeSets)
Set classes with intersection (&&), difference (--) and nesting, plus \p{…} for string properties.
Flags change the behavior of the whole engine and go after the pattern. The regex builder exposes six: g (global, all occurrences), i (case-insensitive), m (multiline, ^ and $ per line), s (dotAll, the dot now matches line breaks), u (Unicode mode, needed for \p{…}) and y (sticky, matches from a fixed position). Modern JavaScript has two more the tool does not use: d (since ES2022, exposes each group’s indices) and v (since ES2024, the superset of u). In find and replace text, ticking "case sensitive" simply turns the i flag on or off.
Pitfalls: escaping, backtracking and ReDoS
To match a metacharacter literally, escape it with a backslash: \. matches a dot, \? matches a question mark, \( matches a parenthesis. Forgetting this is the most common mistake, 3.14 with the pattern 3.14 also matches "3x14", because the dot is a wildcard. A language detail: inside a JavaScript string literal you double the backslash ("\\d"), but in the regex builder’s field you type the pattern directly (\d), with no extra backslash. With escaping out of the way, the truly expensive trap remains.
Catastrophic backtracking happens when the pattern leaves the engine too many ways to split the same text. Take (a+)+ against a string of "a"s that ends in a character that makes the pattern fail, like "aaaaaaaaaaX". The inner a+ can take 1, 2, 3… "a"s; the outer group repeats that; and there are 2^(n-1) ways to partition a run of n "a"s into blocks. Because the trailing X never matches, the engine tries every one of those partitions before giving up. For n = 30 that is over half a billion attempts, the browser freezes. JavaScript’s regex engine is backtracking-based, so it is vulnerable; the regex builder on this page caps at 1000 matches per run precisely so a pathological pattern cannot lock the tab.
View the data
| x | (a+)+, backtracking, 2^(n-1) | a+, linear, n steps |
|---|---|---|
| 2 | 2 | 2 |
| 4 | 8 | 4 |
| 6 | 32 | 6 |
| 8 | 128 | 8 |
| 10 | 512 | 10 |
| 12 | 2,048 | 12 |
| 14 | 8,192 | 14 |
| 16 | 32,768 | 16 |
| 18 | 131,072 | 18 |
| 20 | 524,288 | 20 |
This is not lab theory. On 2 July 2019, Cloudflare went down for about 27 minutes, taking a good chunk of the internet with it, because of a single WAF rule. The culprit was .*(?:.*=.*): two greedy .* fighting over the same text. Against legitimate requests, the engine fell into backtracking and pinned the CPU across the entire global network. In the official post-mortem, Cloudflare was blunt: "the regular expression engine being used didn’t have complexity guarantees". The fix included moving to engines with runtime guarantees, such as RE2 and the Rust regex engine.
How to armor a pattern against ReDoS
Avoid a quantifier inside a quantifier over the same alphabet, (a+)+, (.*)* and (a|a)* are the archetypes. Replace an open .* with a specific class that excludes the delimiter, like [^>]* or [^"]*. Anchor the pattern: a ^ at the start and a $ at the end strip the engine of its backtrack positions. Prefer exact quantifiers ({4}) to loose ranges when you know the length. And always test with a long, deliberately failing input before you trust the pattern.
Since JavaScript has neither atomic groups nor possessives, the standard trick to "lock" a stretch is to wrap it in a capturing lookahead and reference the capture right after: (?=(a+))\1. The lookahead matches a+ greedily, the capture stores the result and the backreference \1 consumes it leaving no backtrack points, the practical effect of an atomic group. Beyond that, validate the input length in code before running the regex: no expression needs to chew through 10 MB at once.
RE2 and linear engines: why they drop lookaround and backreferences
RE2 (from Google) and the Rust regex engine do not backtrack. They compile the pattern into an automaton and scan the input once, with time guaranteed linear in the length of the text, immune to ReDoS. The price is that they refuse to implement features for which only backtracking solutions are known: backreferences and lookaround (lookahead/lookbehind) are left out. In RE2’s own words, it "does not support constructs for which only backtracking solutions are known to exist". It is a conscious trade: you lose two features and gain the guarantee that no malicious input can take the service down, which is exactly why Cloudflare switched.
PCRE, Python and ECMAScript: same bones, different flavors
JavaScript (the ECMAScript standard) is not identical to the PCRE used in PHP, Perl or grep -P, nor to Python’s re module. The basics, anchors, classes, quantifiers, alternation, are the same, but the details diverge: JS has no atomic groups (?>…) or possessives (a++); PCRE anchors \A and \z do not apply in JS (use ^ and $); and property syntax and group naming vary. Before copying a pattern from the internet, confirm which engine it was written for and test in the target one. If the pattern is just one step in a pipeline, pair it with the guide on JSON and YAML conversion to extract and then structure the result.
Frequently asked questions
What is the difference between * and +?
What is catastrophic backtracking (ReDoS) and how do I avoid it in JavaScript?
Can I validate an email with a regex?
Why does \w not match "ç" or "é"?
Does JavaScript have lookbehind and named groups?
How do I stop a regex from "eating" too much text?
Does the same regex work in JavaScript and Python/PHP?
Read every regex by function: anchors pin the position, classes say what fits, quantifiers say how many times and groups organize the rest. Master greedy versus lazy, use \p{L} with the u flag so you never lose accents, and know lookahead, lookbehind and named groups, standard JavaScript since 2018. Above all, distrust nested quantifiers: they cause the catastrophic backtracking that once knocked Cloudflare offline. Prefer specific classes over .*, anchor your patterns and test everything live in the regex builder.
Sources & references
- MDN, Regular expressions (reference guide)
- ECMAScript, RegExp (language specification)
- MDN, Unicode character class escape (\p{…})
- OWASP, Regular expression Denial of Service (ReDoS)
- Cloudflare, Details of the outage on July 2, 2019
- Google RE2, non-backtracking regex engine (linear time)
- WHATWG HTML Standard, valid email address (willful violation of RFC 5322)