Skip to editor content
learningjavascript.orglesson 21 of 25

Regular Expressions

Regular expressions — regex for short — are patterns that describe how to match sequences of characters in a string. Once you understand their syntax, tasks that would take dozens of lines of string manipulation collapse into a single concise expression.

JavaScript has first-class support for regex both as a literal syntax and through built-in methods on strings. This lesson walks you from the basics through practical, real-world patterns.

Creating a Regular Expression

You can write a regex two ways: as a literal using forward slashes, or as a RegExp object using the constructor.

// Literal syntax — preferred for known patterns
const pattern = /hello/;

// Constructor syntax — useful when the pattern comes from a variable
const word = "hello";
const dynamic = new RegExp(word);

const sentence = "Say hello to regular expressions";

console.log(pattern.test(sentence));   // true
console.log(dynamic.test(sentence));   // true
console.log(/goodbye/.test(sentence)); // false

The test method returns true or false — it is the simplest way to check whether a pattern appears in a string.

Flags

Flags modify how a pattern is applied. You place them after the closing slash.

FlagMeaning
gGlobal — find all matches, not just the first
iCase-insensitive
mMultiline — ^ and $ match line boundaries
const text = "The quick brown fox. THE QUICK BROWN FOX.";

// Without flags the match is case-sensitive, and there is no lowercase
// "the" in the string — so .match() returns null rather than an array.
const firstOnly = text.match(/the/);
console.log(firstOnly); // null — always check before indexing a match
console.log(/the/.test(text)); // false — "The" starts with capital T

// Case-insensitive
console.log(/the/i.test(text));          // true
console.log(text.match(/the/gi));        // ["The", "THE"]

// Global match returns all matches as an array
const words = text.match(/\b\w+\b/g);
console.log(words.length); // number of words found
console.log(words.slice(0, 4)); // ["The", "quick", "brown", "fox"]

Character Classes and Quantifiers

Character classes let you match a set of characters. Quantifiers specify how many times a character or group must appear.

Common character classes:

  • \d — any digit (0–9)
  • \w — any word character (letters, digits, underscore)
  • \s — any whitespace character
  • . — any character except a newline
  • [abc] — any of a, b, or c
  • [^abc] — anything except a, b, or c

Common quantifiers:

  • * — zero or more
  • + — one or more
  • ? — zero or one (optional)
  • {n} — exactly n times
  • {n,m} — between n and m times
// Match a date in YYYY-MM-DD format
const datePattern = /\d{4}-\d{2}-\d{2}/;

console.log(datePattern.test("2024-03-15"));  // true
console.log(datePattern.test("24-3-15"));     // false
console.log(datePattern.test("not a date"));  // false

// Extract the date from a string
const log = "Event scheduled for 2024-06-01 in Berlin";
const found = log.match(/\d{4}-\d{2}-\d{2}/);
console.log(found[0]); // "2024-06-01"

// Optional characters — match both "colour" and "color"
const colourPattern = /colou?r/;
console.log(colourPattern.test("colour")); // true
console.log(colourPattern.test("color"));  // true

Debug

This should pull the number out of the string and log 'Amount: 49'. Instead it crashes with a TypeError before printing anything. Predict what throws and why, then fix the pattern so it logs 'Amount: 49'.

const input = "Price: 49 USD";

// Grab the first run of digits from the string.
const match = input.match(/^\$(\d+)/);
const amount = match[1];

console.log("Amount: " + amount);

Expected output: Amount: 49

Capturing Groups

Wrapping part of a pattern in () creates a capturing group. Matched groups are returned alongside the full match when you call match or use exec.

// Parse a time string like "14:35" or "9:05"
const timePattern = /(\d{1,2}):(\d{2})/;

const result = "Meeting at 14:35 tomorrow".match(timePattern);

if (result) {
  const [fullMatch, hours, minutes] = result;
  console.log("Full match:", fullMatch); // "14:35"
  console.log("Hours:", hours);          // "14"
  console.log("Minutes:", minutes);      // "35"
}

// Named groups — cleaner when there are several captures
const named = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const dateStr = "Deadline: 2024-12-31";
const dateResult = dateStr.match(named);

if (dateResult) {
  const { year, month, day } = dateResult.groups;
  console.log(`Year: ${year}, Month: ${month}, Day: ${day}`);
}

Named groups use the (?<name>...) syntax and are accessed through result.groups.

Predict

This pattern has a capturing group AND the g flag. You might expect match() to return the captured digits. Predict exactly what it logs before running it.

const ids = "id-12, id-7, id-3";

const digits = ids.match(/id-(\d+)/g);

console.log(digits);

Replacing with replace and replaceAll

The replace method accepts a regex as its first argument and can reference captured groups in the replacement string using $1, $2, or named groups with $<name>.

// Reformat a date from YYYY-MM-DD to DD/MM/YYYY
const date = "2024-06-15";
const reformatted = date.replace(
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
  "$<day>/$<month>/$<year>"
);
console.log(reformatted); // "15/06/2024"

// Redact phone numbers from user-submitted text
const message = "Call me on 07700 900123 or 07700 900456 thanks";
const redacted = message.replace(/\d{5}\s?\d{6}/g, "[REDACTED]");
console.log(redacted); // "Call me on [REDACTED] or [REDACTED] thanks"

// Use a function as the replacement for dynamic transformations
const template = "Hello, {name}! You have {count} messages.";
const values = { name: "Alice", count: 5 };

const rendered = template.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? "");
console.log(rendered); // "Hello, Alice! You have 5 messages."

Transfer

You just saw replace() reformat a date by referencing captured groups ($<day>/$<month>/$<year>). New task: turn a full name written 'First Last' into 'Last, First' using one replace() call. Which version produces 'Lovelace, Ada' from 'Ada Lovelace'?

// Version A
const a = "Ada Lovelace".replace(/(\w+)\s(\w+)/, "$2, $1");

// Version B
const b = "Ada Lovelace".replace(/\w+\s\w+/, "$2, $1");

// Version C
const c = "Ada Lovelace".replace(/(\w+)\s(\w+)/, "$1, $2");

String Methods That Accept Regex

MethodReturnsUse for
regex.test(str)booleanQuick existence check — a RegExp method, so the receiver and argument are the other way round
str.match(regex)array or nullExtract matches
str.search(regex)index or -1Find position
str.replace(regex, replacement)new stringSubstitute text
str.split(regex)arraySplit on a pattern
const csv = "Alice, 30 ,  engineer ; Bob,  25,  designer";

// Split on either commas or semicolons, trimming surrounding spaces
const entries = csv.split(/\s*[,;]\s*/);
console.log(entries);
// ["Alice", "30", "engineer", "Bob", "25", "designer"]

// Validate a simple email format
const emailPattern = /^[\w.+-]+@[\w-]+\.[a-z]{2,}$/i;
const emails = ["user@example.com", "bad@", "also.good@domain.com", "nope"];

emails.forEach(email => {
  console.log(`${email}: ${emailPattern.test(email) ? "valid" : "invalid"}`);
});

Read that pattern for what it actually accepts, not for what "email" suggests. The domain part is [\w-]+, which cannot match a dot — so a multi-label domain like user@domain.co.uk is reported invalid even though it is a perfectly real address. That is not a bug to patch here; it is the point. Every simple email regex rejects addresses that are valid and accepts ones that are not, which is why production code confirms an address by sending mail to it rather than by trusting a pattern.

Recall

Without scrolling up: you split a string on a regex and get back an array of pieces, but some pieces are empty strings you want gone. How do you clean it up, and why does that approach work on the result of split?

Try It Yourself

This is a build task: a small program that reports its own pass/fail. Three functions are stubbed out with only their signatures — no worked example inside them — and the checks below fail until each returns the right value. Run it as-is to see the first failure, then design the patterns yourself. You have the full toolkit now: a capturing group applied globally (Capturing Groups — here via matchAll, which returns an iterator of match arrays, one per occurrence, each shaped like a match result), replace with a whitespace class and a quantifier (Replacing with replace), and an anchored test (Flags, Character Classes, and the ^/$ anchors from the email example).

The spec, once:

  • extractPrices(text) returns an array of every dollar amount as a Number, in order — e.g. $4.50 becomes 4.5, $3 becomes 3. A price is a $ followed by digits, optionally a dot and more digits.
  • normalize(text) returns the text with runs of whitespace collapsed to a single space, the ends trimmed, and everything lowercased.
  • isValidIdentifier(name) returns true only if name is a valid identifier: it starts with a letter or underscore, then contains only letters, digits, or underscores, and nothing else — so the pattern must match the whole string.

Build

Finish the build. Three functions are stubbed with just their signatures and the checks below them fail until each returns the right value. Run it as-is to see which check fails first, design the pattern that function needs, and work down until it prints 'All checks passed.' The checks run top to bottom, so the first failure is extractPrices — implement it first.

const assert = require('assert');

const receipt = "Coffee $4.50, bagel $3, tip $1.25 — total due soon.";

// TODO 1: return an array of every dollar amount as a Number, in order.
function extractPrices(text) {
// your code here
}

// TODO 2: return the text with whitespace runs collapsed to one space, ends trimmed, lowercased.
function normalize(text) {
// your code here
}

// TODO 3: return true only if name is a valid identifier (letter/underscore first, then letters/digits/underscores, whole string).
function isValidIdentifier(name) {
// your code here
}

// --- Build checks: these must all pass. Do not edit below this line. ---
assert.deepStrictEqual(
extractPrices(receipt),
[4.5, 3, 1.25],
"extractPrices(receipt) should pull every $-amount out as a number, in order",
);
assert.strictEqual(
normalize("  Grumpy   Cat   RULES  "),
"grumpy cat rules",
"normalize should collapse runs of whitespace, trim the ends, and lowercase",
);
assert.strictEqual(
isValidIdentifier("total_2"),
true,
"total_2 is a valid identifier: starts with a letter, then letters/digits/underscore",
);
assert.strictEqual(
isValidIdentifier("2fast"),
false,
"2fast is invalid: an identifier may not start with a digit",
);

console.log("All checks passed.");
console.log("Prices:", extractPrices(receipt));
console.log("Normalized:", normalize("  Grumpy   Cat   RULES  "));
console.log("total_2 valid?", isValidIdentifier("total_2"));
console.log("2fast valid?", isValidIdentifier("2fast"));

Expected output: All checks passed. Prices: [ 4.5, 3, 1.25 ] Normalized: grumpy cat rules total_2 valid? true 2fast valid? false

Once it passes, try two variations and predict each before running:

  1. Drop matchAll for match with g. Rewrite extractPrices as text.match(/\$(\d+(?:\.\d+)?)/g).map(Number). Predict what it returns before running. With the g flag, match discards the capturing group and returns the full matches ['$4.50', '$3', '$1.25'] — and Number('$4.50') is NaN, so you get [ NaN, NaN, NaN ]. That is the same "g drops groups" trap from the Predict above: to keep the captured digits across all matches you need matchAll, not match with g.
  2. Drop the .trim(). Remove .trim() from normalize so it is just .replace(/\s+/g, " ").toLowerCase(). Predict what normalize(" Grumpy Cat RULES ") returns before running. The leading and trailing whitespace runs each collapse to a single space rather than vanishing, so you get " grumpy cat rules " — with a stray space at both ends. Collapsing a run is not the same as removing it at the edges; trim (or anchored ^\s+|\s+$) is what clears the ends.

Key Takeaways

  • Create regex with /pattern/flags literals for static patterns and new RegExp(str) for dynamic ones.
  • test checks for a match; match extracts matches into an array (or returns null).
  • Character classes (\d, \w, \s, [...]) and quantifiers (+, *, ?, {n,m}) are the building blocks of most patterns.
  • The g flag makes match return every occurrence; without it you only get the first.
  • Capturing groups (...) let you extract sub-parts of a match; named groups (?<name>...) make replacements and destructuring clearer.
  • replace with a callback function gives you full control over how each match is transformed.
  • Anchor your patterns with ^ and $ when you need to match the entire string, not just part of it.

Pro Tip: Use a tool like regex101.com to build and debug patterns interactively — it highlights matches, explains each token, and lets you test against multiple strings in real time. Always add the u flag to new patterns (/pattern/u) to enable full Unicode matching and catch subtle bugs with emoji or non-ASCII characters.

Next Steps

You can now match and transform any string pattern. Next, you will learn how to work with dates and times in JavaScript — parsing, formatting, and doing arithmetic with one of the language's trickiest built-in types.

Next lesson

Date and Time

Master JavaScript's Date object to create, format, compare, and manipulate dates and times for building real-world applications.

25 min