Regular expressions explained: character classes, greedy vs lazy quantifiers, groups, flags and lookarounds, a regex cheat sheet, and catastrophic backtracking.
By Udit Agarwal · Updated October 17, 2026
A regular expression, or regex, is a pattern that describes a set of strings, used to search, validate, extract and replace text. \d{3}-\d{4} finds a number like 555-0123, and ^\d+$ checks that a whole string is digits. Most languages and editors share the core syntax, though flavors differ in the details; the examples here use JavaScript's, which browsers and the ArrayKit Regex Tester run.
Most characters match themselves: cat finds those three letters anywhere, including inside concatenate. A handful have special meanings and need a backslash to be matched literally: . ^ $ * + ? ( ) [ ] { } | \. So \. matches a period and \? a question mark.
A character class in square brackets matches one character from a set. [aeiou] matches one vowel, [a-z] one lowercase letter, and [^0-9] any character that is not a digit. Shorthand classes cover the common sets: \d is a digit, \w a word character (A–Z, a–z, 0–9 and underscore), \s whitespace, and the uppercase versions \D, \W and \S are their opposites. A dot matches any character except a line break.
One JavaScript surprise: \w only knows ASCII, even with the u flag. Run against Ünïcödé it finds just n, c and d. For letters in any language, use \p{L} with the u flag.
Anchors match positions, not characters. ^ is the start of the input and $ the end, while \b is a word boundary, so \bcat\b matches cat in "the cat sat" but not in "concatenate". Anchors are what turn a search into a validation: \d+ finds 123 inside abc123, while ^\d+$ rejects it.
A quantifier says how many times the item before it may repeat:
* zero or more, + one or more, ? zero or one, so colou?r matches both "color" and "colour".{3} exactly three, {2,} two or more, {2,5} between two and five.Quantifiers are greedy by default: they take as much as they can, then give characters back only if the rest of the pattern fails. On <b>bold</b> text, the pattern <.+> matches all of <b>bold</b>, because .+ runs to the last >. Adding ? makes a quantifier lazy, taking as little as possible, so <.+?> matches <b> and </b> separately. A negated class is often better still: <[^>]+> cannot run past a >.
Parentheses group part of a pattern, as in (ha)+, and capture what that part matched. Run against Released 2026-10-17., the pattern (\d{4})-(\d{2})-(\d{2}) captures 2026, 10 and 17. Named groups label the parts: (?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2}). When you need grouping without capturing, write (?:…).
Captures are what make replacements useful. In JavaScript, '2026-10-17'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1') returns 17/10/2026.
The pipe means "or", and it has the lowest precedence of anything in a pattern. ^cat|dog$ means "starts with cat, or ends with dog", so it matches catdog. To accept exactly one of the words, group them: ^(?:cat|dog)$.
A backreference matches the same text a group captured earlier, using \1 or \k<name>. With the g and i flags, \b(\w+)\s+\1\b finds doubled words, flagging is is, test test and The the in "This is is a test test. The the end."
Lookarounds check what comes before or after a position without including it in the match: (?=…) means "followed by", (?!…) "not followed by", (?<=…) "preceded by" and (?<!…) "not preceded by".
\d+(?= dollars) finds 100 in "100 dollars and 200 euros".(?<=\$)\d+(?:\.\d{2})? finds 42.50 and 3 in "Total: $42.50, tax $3, 7 items".^(?=.*\berror\b)(?=.*\bdisk\b) matches lines containing both words, in either order.Stacked lookaheads are how sites enforce rules such as "at least one digit", although current NIST guidance tells systems not to impose them (see what makes a password strong). Watch how lookarounds interact with backtracking: \d+(?! dollars) still matches 10 in "100 dollars", because 10 is followed by "0 dollars". Adding boundaries, as in \b\d+\b(?! dollars), fixes it.
Flags change how the whole pattern behaves:
| Flag | Name | Effect |
|---|---|---|
g |
global | Find every match, not just the first |
i |
ignore case | hello also matches HELLO |
m |
multiline | ^ and $ also match at line breaks |
s |
dotAll | . also matches line breaks |
u |
unicode | Works on full code points and enables \p{…} |
y |
sticky | Matches only at the exact position in lastIndex |
In three lines reading "12", "ab" and "34", ^\d+$ finds nothing until m is on, then finds 12 and 34. Without u, ^.$ does not match the single emoji 😀, because JavaScript stores it as two UTF-16 units, and \p{L} silently matches the literal text p{L}. Recent engines add d (match positions for groups) and v, a stricter successor to u.
One trap in code: a regex with g remembers where its last match ended, so repeated .test() calls on the same object can alternate between true and false on identical input.
| Pattern | Matches |
|---|---|
. |
Any character except a line break |
\d \w \s |
Digit, word character, whitespace (\D \W \S negate them) |
[abc] [^abc] [a-z] |
One of, none of, a range |
^ $ |
Start and end of input, or of a line with m |
\b \B |
Word boundary, not a word boundary |
* + ? |
Zero or more, one or more, optional |
{n} {n,} {n,m} |
Exactly n, at least n, n to m |
*? +? ?? |
Lazy versions |
(…) (?:…) (?<name>…) |
Capture, group only, named capture |
\1 \k<name> |
Backreference |
a|b |
a or b |
(?=…) (?!…) |
Followed by, not followed by |
(?<=…) (?<!…) |
Preceded by, not preceded by |
\. \\ |
A literal dot, a literal backslash |
| Task | Pattern | Limit |
|---|---|---|
| Rough email check | ^[^\s@]+@[^\s@]+\.[^\s@]+$ |
Accepts ada@example..com and rejects valid quoted addresses |
| Date in YYYY-MM-DD form | ^\d{4}-\d{2}-\d{2}$ |
Accepts 2026-13-45 |
| US ZIP code | ^\d{5}(?:-\d{4})?$ |
Checks the format, not that the code exists |
| Hex color | ^#(?:[0-9a-fA-F]{3}){1,2}$ |
Rejects 8-digit colors with alpha, like #1e90ff80 |
| Trailing spaces | [ \t]+$ with g and m |
Ignores other Unicode spaces |
| Doubled words | \b(\w+)\s+\1\b with g and i |
ASCII words only |
A regex can only check the shape of a value. Even the stricter date pattern ^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$ accepts 2026-02-31, and no pattern can tell whether an email address actually exists.
The Regex Tester's opening example is a simple email finder, \b[\w.]+@[\w.]+\.\w+\b. It shortens user+tag@example.com to tag@example.com and misses ada@my-site.com entirely, because neither + nor - is in [\w.]. For structured text such as URLs, a parser beats a pattern: the URL Parser splits an address properly, and URL encoding, explained shows why the same address can be written several ways.
When part of a match fails, the engine backs up and tries other ways of dividing the text among the quantifiers. Usually that is cheap, but when quantifiers are nested over the same characters, the number of ways grows exponentially and a failing match tries every one.
^(a+)+$ is the textbook case. Against a run of as ending in !, each extra a roughly doubles the work: in Node.js on a laptop, 24 of them took about 0.1 seconds, 28 about 3 seconds and 30 about 8 seconds. Real patterns hide the same flaw. ^(\w+\s?)*$, meant as "words separated by single spaces", took about 45 seconds to reject a 50-character line of ten words ending in !. Rewritten as ^(?:\w+\s)*\w*$, which accepts exactly the same strings, it rejected a 10,000-word line in under a millisecond.
When attackers trigger it on purpose it is called ReDoS, regular expression denial of service, and it has caused real outages. In July 2019, a single regular expression in a firewall rule backtracked so heavily that it exhausted the CPUs serving Cloudflare's traffic and took its network down for 27 minutes. To stay safe:
(a+)+, (\w+\s?)* or (.*)*.[^"]* to .*, and make alternatives mutually exclusive.The Regex Tester runs your pattern with JavaScript's built-in RegExp engine, so it follows the same rules as browsers and Node.js. Type the pattern without the surrounding slashes, paste a test string, and switch the g, i, m, s and u flags on or off. It highlights every match in the text, counts them, and lists the first 50 with each match's start position, its text, its numbered groups (an unmatched group shows as an empty string) and its named groups. An invalid pattern shows the engine's own error message.
The tester always lists every match, even with g switched off, and it stops collecting after about 5,000 matches. It has no replace mode and no toggle for y, d or v. It re-runs the pattern as you type, inside the page, so a catastrophic pattern can freeze the tab; try risky patterns on short text first. Patterns and text stay in your browser and are not uploaded.
Shell-style wildcards are a different syntax: Glob to Regex converts a pattern like src/**/*.ts into a JavaScript regex and tests it against sample paths.
What is a regular expression? A regular expression is a pattern that describes a set of strings. Programs use it to find, validate, extract or replace text, for example finding every date in a log or checking that a field contains only digits.
What is the difference between greedy and lazy matching? A greedy quantifier matches as much text as it can and gives some back only if needed, while a lazy one, written with an extra question mark such as +?, matches as little as possible.
What does the g flag do? It makes a regex find every match instead of stopping at the first. In JavaScript it also makes the regex remember where the last match ended, which affects repeated calls to test() and exec().
Can a regex validate an email address? Only roughly. The address format allows far more variety than a practical pattern accepts, and a well-formed address may not exist. Use a simple pattern to catch typos and confirm the address by sending an email.
Which regex flavor does the ArrayKit Regex Tester use? JavaScript's. It runs the browser's own RegExp engine, so a pattern behaves there as it will in your JavaScript code, with the g, i, m, s and u flags available as toggles.
What is catastrophic backtracking? It is what happens when a pattern can match the same text in exponentially many ways, often because of nested quantifiers. A failing match then tries every combination and can take seconds, minutes or longer on a short string. Rewriting the pattern so each character can be matched only one way fixes it.
All Code Formatters tools · All guides