Regular expressions
A regular expression (regex, or regexp) is a pattern that describes text: "six digits", "a word starting with a capital letter", "anything that looks like an email address". With one you can check whether text matches, find every match, extract parts, replace matches and split text. Regexes look cryptic at first, but they are built from a small set of pieces, and this lesson goes through them one at a time.
Writing a regex
A regex is written between slashes, optionally followed by flags:
const pattern = /cat/; // a regex literal: matches the letters c, a, t
console.log(pattern.test("concatenate")); // true: "cat" appears inside
console.log(pattern.test("Cat")); // false: case matters
console.log(/cat/i.test("Cat")); // the i flag ignores case
const word = "dog";
const built = new RegExp(word, "i"); // build one from a string at run time
console.log(built.test("Hot DOG"), built.source, built.flags);The methods that use regexes
| Method | Does | Returns |
|---|---|---|
regex.test(text) | is there a match? | true / false |
text.match(regex) | the first match (or all, with g) | an array, or null |
text.matchAll(regex) | every match, with details (needs g) | an iterator |
text.search(regex) | where the first match starts | an index, or -1 |
text.replace(regex, x) | replaces the first match (all, with g) | a new string |
text.split(regex) | splits wherever it matches | an array |
const text = "Order 12 shipped, order 7 pending, order 305 delivered";
console.log(/\d+/.test(text));
const first = text.match(/\d+/); // an array holding the first match...
console.log(first[0], "at index", first.index); // ...and .index, where it starts
console.log(text.match(/\d+/g)); // every match
console.log(text.search(/pending/));
console.log(text.replace(/order/gi, "#"));
console.log("a, b;c d".split(/[,;\s]+/));Character classes: which characters
| Pattern | Matches |
|---|---|
. | any one character except a new line |
\d / \D | a digit 0 to 9 / anything but a digit |
\w / \W | a "word" character (letter, digit, _) / anything else |
\s / \S | whitespace (space, tab, new line) / anything else |
[abc] | one of a, b or c |
[a-z], [A-Z0-9] | one character in a range |
[^abc] | one character that is not a, b or c |
console.log("R2-D2 and C-3PO".match(/\d/g));
console.log("hello world".match(/[aeiou]/g).length, "vowels");
console.log("Room 101, floor 3".replace(/\D/g, "")); // keep only the digits
console.log("tab\tand space".split(/\s/));Quantifiers: how many
| Pattern | Means |
|---|---|
x* | zero or more x |
x+ | one or more x |
x? | zero or one x (optional) |
x{3} | exactly 3 |
x{2,} | 2 or more |
x{2,4} | from 2 to 4 |
console.log(/colou?r/.test("color"), /colou?r/.test("colour")); // the u is optional
console.log("aaa bb c".match(/b+/)[0]);
console.log(/^\d{6}$/.test("411001"), /^\d{6}$/.test("41100")); // exactly 6 digits
console.log("haaaappy".replace(/a{2,}/, "a"));Greedy and lazy
Quantifiers are greedy: they match as much as they can. Adding ? after one makes it lazy: as little as possible:
const html = "<b>bold</b> and <i>italic</i>";
console.log(html.match(/<.+>/)[0]); // greedy: from the first < to the LAST >
console.log(html.match(/<.+?>/)[0]); // lazy: stops at the first >
console.log(html.match(/<.+?>/g));Anchors: where
| Pattern | Means |
|---|---|
^ | the start of the text (or of a line, with m) |
$ | the end of the text (or of a line, with m) |
\b | a word boundary: between a word character and anything else |
Validation patterns almost always use ^...$, so the whole text must match, not just part of it:
console.log(/\d{4}/.test("abc12345xyz")); // true: contains 4 digits somewhere
console.log(/^\d{4}$/.test("abc12345xyz")); // false: must be exactly 4 digits
console.log("cat catalog scatter".match(/\bcat\b/g)); // only the whole wordGroups and alternation
Round brackets ( ) group part of a pattern, so a quantifier can apply to the whole group, and they capture what that part matched. | means "or":
console.log(/^(ha)+!$/.test("hahaha!"));
console.log(/^(cat|dog)s?$/.test("dogs"), /^(cat|dog)s?$/.test("cows"));
const date = "2026-10-31";
const m = date.match(/^(\d{4})-(\d{2})-(\d{2})$/);
console.log(m[0], "| year", m[1], "| month", m[2], "| day", m[3]);
const named = date.match(/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/);
console.log(named.groups.day + "/" + named.groups.month + "/" + named.groups.year);m[0] is the whole match and m[1], m[2]... are the groups in order. Named groups (?<name>...) are easier to read: match.groups.name. A group that should group but not capture is written (?:...).
Groups in replacements
In replace, $1, $2 (or $<name>) insert what a group captured. Or pass a function, which receives the match and the groups and returns the replacement:
console.log("2026-10-31".replace(/(\d{4})-(\d{2})-(\d{2})/, "$3/$2/$1"));
console.log("Asha Rao".replace(/(?<first>\w+) (?<last>\w+)/, "$<last>, $<first>"));
const title = "the quick brown fox".replace(/\b\w/g, (letter) => letter.toUpperCase());
console.log(title);
const prices = "pen 10, pad 45, bag 900";
console.log(prices.replace(/\d+/g, (n) => Number(n) * 2)); // double every numbermatchAll: every match with its details
const tweet = "Learning #javascript and #regex on #CodeMIRAI";
for (const m of tweet.matchAll(/#(\w+)/g)) {
console.log(`tag "${m[1]}" at position ${m.index}`);
}
console.log([...tweet.matchAll(/#(\w+)/g)].map((m) => m[1].toLowerCase()));Flags
| Flag | Effect |
|---|---|
g | global: find all matches, not just the first |
i | ignore case |
m | multiline: ^ and $ match at every line |
s | "dotAll": . matches new lines too |
u | Unicode: handle emoji and other characters correctly |
const lines = "apple\nbanana\navocado";
console.log(lines.match(/^a\w+/g)); // only the start of the whole text
console.log(lines.match(/^a\w+/gm)); // the start of every lineEscaping special characters
These characters have special meanings: . * + ? ^ $ { } ( ) [ ] | \ /. To match one literally, put a backslash before it:
console.log(/3.14/.test("3x14")); // true! the dot matches any character
console.log(/3\.14/.test("3x14")); // false: \. is a real dot
console.log("1+1=2".replace(/\+/g, " plus "));
console.log(/^\$\d+$/.test("$100"));In new RegExp("..."), the pattern is a string, so each backslash must be doubled: new RegExp("\\d+").
Lookaheads: rules without consuming
(?=...) checks that something follows, without including it in the match; (?!...) checks that it doesn't. Several lookaheads at the start of a pattern can check several rules at once, which is how password rules are often written:
const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/; // lower, upper, digit, 8+ characters
for (const p of ["password", "Password1", "Pass1", "PASSWORD99", "learnJS2026"]) {
console.log(p.padEnd(12), strong.test(p));
}
console.log("price: 100USD, 250INR".match(/\d+(?=INR)/)[0]); // the number before INRPractical patterns
const patterns = {
pinCode: /^[1-9]\d{5}$/, // Indian PIN: 6 digits, not starting with 0
mobile: /^[6-9]\d{9}$/, // Indian mobile: 10 digits starting 6-9
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, // simple: something@something.something
hexColour: /^#([0-9a-f]{3}|[0-9a-f]{6})$/i,
time24: /^([01]\d|2[0-3]):[0-5]\d$/,
};
const tests = { pinCode: ["411001", "011001"], mobile: ["9876543210", "1234567890"], email: ["asha@example.com", "asha@example"],
hexColour: ["#0aa9cf", "#xyz"], time24: ["23:59", "24:00"] };
for (const [name, values] of Object.entries(tests)) {
console.log(name.padEnd(10), values.map((v) => `${v} ${patterns[name].test(v) ? "ok" : "no"}`).join(" | "));
}
// tidy messy spacing
console.log(" too many spaces ".trim().replace(/\s+/g, " "));When not to use a regex
Regexes are great for patterns in text, but poor for structured formats with nesting, like HTML or JSON: use DOMParser and JSON.parse instead. And for a plain search, includes, startsWith or split(",") are clearer. A regex nobody can read is a bug waiting to happen: keep them short, add a comment saying what they match, and test them on examples (sites like regex101.com explain a pattern piece by piece).
Key points
- Write regexes as
/pattern/flags(ornew RegExp(string, flags)). testchecks,match/matchAllfind,replacechanges,splitcuts.\d \w \s .and[...]say which characters;* + ? {n,m}say how many;^ $ \bsay where.( )groups and captures ($1, named(?<n>...)),|means or,\escapes special characters.gfinds all,iignores case,mmakes^/$work per line.- Anchor validation patterns with
^...$.
Practice
- Check whether a string is exactly 4 digits (a year like
2026). - Find every word that starts with a capital letter in
"Asha met Leo in Pune on Monday". - Turn
"31/10/2026"into"2026-10-31"with onereplace. - Count the sentences in a paragraph by splitting on
.,!or?. - Write a pattern for a username: 3 to 16 letters, digits or underscores, starting with a letter.