A regular expression, or regex, is a small pattern language for describing text, such as "four digits, a hyphen, two digits". JavaScript uses regexes to test, search, split and replace strings. They are compact and easy to get wrong, so every pattern here is shown with inputs it does and does not match, and the lesson ends with cases where a regex is the wrong tool.
Creating a regex
There are two ways to create one:
const literal = /colou?r/i; // regex literal
const built = new RegExp("colou?r", "i"); // constructor, from a string
Use the literal form when the pattern is fixed. Use new RegExp() when you build the pattern from a variable. In the string form, every backslash must be doubled, because the string itself uses backslash escapes:
const digits = new RegExp("\\d+");
console.log(digits.test("abc 42")); // → true
Testing for a match
regex.test(string) returns true or false:
const colour = /colou?r/i;
console.log(colour.test("Colour")); // → true
console.log(colour.test("color")); // → true
console.log(colour.test("colr")); // → false
u? means "an optional u", and the i flag makes the match case-insensitive. Notice that test looks for the pattern anywhere in the string, so /colou?r/ also matches "watercolours". To match the whole string, anchor it with ^ (start) and $ (end).
Pattern syntax
Characters and character classes
| Pattern | Matches |
|---|---|
abc | The literal characters abc |
. | Any single character except a line break |
[aeiou] | One character from the set |
[a-z0-9] | One character in a range |
[^0-9] | One character not in the set |
\d | A digit, 0 to 9 |
\w | A "word" character: A to Z, a to z, 0 to 9 and _ |
\s | Whitespace: space, tab, line break and similar |
\D, \W, \S | The opposite of \d, \w, \s |
\b | A word boundary (a position, not a character) |
To match a character that has a special meaning, such as ., *, ?, ( or /, escape it with a backslash: \. matches a full stop.
\w only covers ASCII letters, so it does not match "é" or "ö"; the flags section shows the fix.
Anchors
| Pattern | Matches |
|---|---|
^ | The start of the string (or of a line with the m flag) |
$ | The end of the string (or of a line with the m flag) |
\b | A boundary between a word character and a non-word character |
console.log(/\bcat\b/.test("the cat sat")); // → true
console.log(/\bcat\b/.test("concatenate")); // → false
Quantifiers
| Pattern | Means |
|---|---|
? | Zero or one |
* | Zero or more |
+ | One or more |
{3} | Exactly three |
{2,4} | Between two and four |
{2,} | Two or more |
A quantifier applies to the item immediately before it: one character, one class or one group.
Groups and alternation
Parentheses group part of a pattern so a quantifier can apply to the whole group, and they capture what that part matched. (?:...) groups without capturing. A vertical bar means "or".
Here is a pattern for the URL slugs this course uses: lowercase letters and digits, separated by single hyphens:
const slug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
console.log(slug.test("javascript-promises")); // → true
console.log(slug.test("css-grid-2")); // → true
console.log(slug.test("JavaScript")); // → false (capitals)
console.log(slug.test("trailing-")); // → false
console.log(slug.test("double--dash")); // → false
console.log(slug.test("has space")); // → false
Read it piece by piece: ^ start, [a-z0-9]+ one or more lowercase letters or digits, (?:-[a-z0-9]+)* zero or more "hyphen plus letters or digits" groups, $ end. Every hyphen must be followed by a character, so trailing and double hyphens fail.
Greedy and lazy quantifiers
Quantifiers are greedy: they match as much as they can. Adding ? after a quantifier makes it lazy, matching as little as possible:
const text = 'say "hi" and "bye"';
console.log(text.match(/".*"/)[0]); // → "hi" and "bye"
console.log(text.match(/".*?"/)[0]); // → "hi"
The greedy .* ran to the last quote; the lazy .*? stopped at the first one that completed the match. A negated class, /"[^"]*"/, is often clearer still.
Flags
Flags go after the closing slash, or as the second argument to new RegExp().
| Flag | Effect |
|---|---|
g | Global: find every match, not just the first |
i | Case-insensitive |
m | Multiline: ^ and $ match at the start and end of each line |
s | "dotAll": . also matches line breaks |
u | Unicode: treat the pattern and input as Unicode code points, and allow \p{...} |
v | An upgraded u mode with set operations in classes (ES2024, supported in all current browsers) |
y | Sticky: match only at the position in lastIndex |
d | Record the start and end index of each captured group |
The u flag enables Unicode property escapes, which are the reliable way to match letters in any language:
Finding matches
match without g: the first match and its groups
const date = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = "Updated on 2026-09-14".match(date);
console.log(result[0]); // → 2026-09-14 (the whole match)
console.log(result.index); // → 11 (where it starts)
console.log(result.groups.year); // → 2026
console.log(result.groups.day); // → 14
console.log(result[1]); // → 2026 (groups are also numbered)
(?<year>...) is a named capturing group. Names make the pattern self-documenting and let you read results with result.groups.year instead of counting brackets. If there is no match, match returns null, so check before reading properties, or use optional chaining: text.match(date)?.groups.year.
The dashboard's updated dates use this format. For real date arithmetic, though, use Date or Intl from lesson 27.
match with g: every match, no groups
const text = "Tagged #javascript, #async and #css.";
console.log(text.match(/#\w+/g)); // → [ '#javascript', '#async', '#css' ]
With g, match returns just the matched strings, without group details, or null when nothing matches.
matchAll: every match with its groups
for (const match of text.matchAll(/#(\w+)/g)) {
console.log(match[1], match.index);
}
// → javascript 7
// → async 20
// → css 31
matchAll requires the g flag (it throws a TypeError without it) and returns an iterator of full match objects.
search and split
string.search(regex) returns the index of the first match, or -1. string.split() accepts a regex as the separator:
console.log("html, css ,javascript".split(/\s*,\s*/));
// → [ 'html', 'css', 'javascript' ]
Replacing
replace swaps the first match; with the g flag, or with replaceAll, it swaps every match:
console.log(" too many spaces ".trim().replace(/\s+/g, " "));
// → too many spaces
In the replacement string, $1, $2 and so on insert numbered groups, and $<name> inserts a named group:
const isoDate = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;
console.log("2026-09-14 and 2025-01-02".replace(isoDate, "$<day>/$<month>/$<year>"));
// → 14/09/2026 and 02/01/2025
You can also pass a function that receives the match and returns the replacement. replaceAll with a regex requires the g flag.
Building a pattern from user input
The dashboard's search box could build a regex from what the user types. But characters such as +, ( and . have special meanings, so a search for "c++" produces an invalid pattern:
new RegExp("c++"); // SyntaxError: Invalid regular expression: /c++/: Nothing to repeat
Escape user input before putting it in a pattern:
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const term = "c++";
const search = new RegExp(escapeRegExp(term), "i");
console.log(search.source); // → c\+\+
console.log(search.test("Learn C++ today")); // → true
$& in the replacement inserts the whole match, so every special character gets a backslash in front. For a plain "contains" search, though, you do not need a regex at all: title.toLowerCase().includes(term.toLowerCase()) is simpler and cannot fail.
The lastIndex trap
A regex with the g or y flag remembers where its last match ended in its lastIndex property, and test and exec start searching from there:
const re = /css/g;
console.log(re.test("css")); // → true (lastIndex is now 3)
console.log(re.test("css")); // → false (searched from index 3, found nothing, reset to 0)
console.log(re.test("css")); // → true
This bites when one global regex tests many strings, for example inside a filter. Do not use the g flag with test; keep it for match, matchAll, replace and replaceAll.
When a regex is the wrong tool
Regexes describe patterns in flat text. Many jobs look like that but are not:
| Task | Use instead |
|---|---|
| Parsing HTML | DOMParser, or DOM methods on elements you already have |
| Parsing URLs and query strings | new URL() and URLSearchParams |
| Parsing JSON | JSON.parse() |
| Checking if a string starts or ends with something | startsWith(), endsWith(), includes() |
| Validating email addresses fully | type="email" and a confirmation email; a regex can only reject obvious mistakes |
| Dates and numbers for display | Intl.DateTimeFormat, Intl.NumberFormat |
Some patterns take exponential time on certain inputs. Nested quantifiers such as (a+)+ or (\w+\s?)* followed by something that fails to match can make the engine try a huge number of combinations, freezing the page or a server. This is called catastrophic backtracking, and attackers exploit it with crafted input (a "ReDoS" attack). Keep patterns simple, avoid nested quantifiers, and limit input length before matching.
Summary
- Create regexes with
/pattern/flags, ornew RegExp(string, flags)with doubled backslashes. - Use character classes (
[a-z],\d,\w,\s), anchors (^,$,\b), quantifiers (?,*,+,{n,m}) and groups to build patterns. - Quantifiers are greedy; add
?for lazy matching. testchecks;matchandmatchAllextract;replaceandreplaceAllrewrite;splitsplits.- Named groups (
(?<name>...)) are read withmatch.groups.nameand inserted with$<name>. - Use the
uflag and\p{L}for letters in any language. - Escape user input before building a pattern, and do not use the
gflag withtest. - Prefer
URL,JSON.parse, DOM APIs,Intland simple string methods when they fit the job, and avoid nested quantifiers.