Regular Expressions

Advanced · 18 min read · ▶ live playground · ✦ checkpoint

You do not need a 30-line parser to answer "does this username look valid?" or "pull the order number out of this receipt." A regular expression, usually shortened to regex, is a compact pattern for finding text. Treat it like a scalpel: excellent for precise cuts, terrible when you swing it like a chainsaw.

Literals, RegExp, and test

A regex pattern is the text-search rule inside a regular expression. The easiest way to write one is a regex literal, a pattern between slashes:

const usernamePattern = /^[a-z][a-z0-9_]{2,15}$/;
 
console.log(usernamePattern.test("mina_42"));
console.log(usernamePattern.test("42mina"));

true, then false.

test asks a yes/no question: "does this string match?" That makes it perfect for validation.

Read the pattern in pieces:

  • ^ means "start of the string."
  • [a-z] means "one lowercase letter."
  • [a-z0-9_] means "one lowercase letter, digit, or underscore."
  • {2,15} means "repeat the previous thing 2 through 15 times."
  • $ means "end of the string."

A character class is a bracketed set of allowed characters, such as [a-z] or [0-9]. A quantifier is a repeat rule, such as {2,15}, +, *, or ?.

You can also create a regex with the RegExp object, which is useful when part of the pattern comes from a variable:

const badgePrefix = "VIP";
const badgePattern = new RegExp(`^${badgePrefix}-\\d{3}$`);
 
console.log(badgePattern.test("VIP-204"));
console.log(badgePattern.test("vip-204"));

true, then false.

In regex, \d means "one digit." In a JavaScript string passed to new RegExp(...), you write \\d because the string parser consumes one backslash first.

If the variable text comes from a user, do not drop it straight into new RegExp(...). ES2025 finalized RegExp.escape(...) for escaping user text before building a pattern; use that helper when your runtime supports it, or use a vetted escape helper.

match and replace

Use match when you want the matching text back. Without the g flag, it returns the first match with extra details. With g, it returns all matching text pieces.

const receipt = "Mina paid with order ORD-1042. Rae paid with order ORD-2048.";
 
const firstOrder = receipt.match(/ORD-\d{4}/);
const allOrders = receipt.match(/ORD-\d{4}/g);
const cleaned = receipt.replace(/ORD-(\d{4})/g, "#$1");
 
console.log(firstOrder[0]);
console.log(firstOrder.index);
console.log(allOrders);
console.log(cleaned);

ORD-1042, 21, ['ORD-1042', 'ORD-2048'], then Mina paid with order #1042. Rae paid with order #2048.

A flag is a letter after the closing slash that changes how the regex behaves. The g flag means global search: keep finding matches instead of stopping at the first one.

The parentheses in /ORD-(\d{4})/g create a capture group, which means "remember this part of the match." In the replacement string, $1 means "the first captured group." So ORD-1042 becomes #1042.

Groups and named captures

A group is a parenthesized part of a regex that keeps pieces together. Groups let you capture text, apply a quantifier to several characters, or offer choices with |, which means "or."

Named capture groups make extracted data easier to read. A named capture group is a capture group written (?<name>...), so the result has a groups.name property.

const orderLine = "Mina: ORD-1042, Rae: REF-2048";
 
const firstCode = orderLine.match(/(?<kind>ORD|REF)-(?<number>\d{4})/);
const normalized = orderLine.replace(
  /(?<kind>ORD|REF)-(?<number>\d{4})/g,
  "$<kind> #$<number>",
);
 
console.log(firstCode.groups.kind);
console.log(firstCode.groups.number);
console.log(normalized);

ORD, 1042, then Mina: ORD #1042, Rae: REF #2048.

This is where regex starts to earn its keep: not just "is there a code?", but "which kind of code did we find, and what number did it carry?"

Still, keep the scalpel rule. Regex is good for stable text patterns: order IDs, usernames, hashtags, quick cleanup, simple validation. It is a poor tool for full programming languages, deeply nested formats, or "is this email address valid in every possible legal case?" For those, use a parser or a library that owns the messy rules.

Try it — clean signup text

Run this once, then change the third signup line from sol.dev to sol_dev. Watch which rows survive and how the replacement formats the names.

javascript — live consolereal engine
⌘/Ctrl + Enter to run

The g and y flags

The g flag searches the whole string. The y flag, called sticky, only matches exactly at the regex's current lastIndex. Think of g as a scout walking ahead until it finds the next tag, and y as a turnstile that only opens if the next tag starts right where the scanner is standing.

const tagText = "#PAID#SHIPPED later #DONE";
 
console.log(tagText.match(/#[A-Z]+/g));
 
const tagScanner = /#[A-Z]+/y;
 
tagScanner.lastIndex = 0;
console.log(tagScanner.exec(tagText)[0]);
console.log(tagScanner.lastIndex);
 
tagScanner.lastIndex = 5;
console.log(tagScanner.exec(tagText)[0]);
console.log(tagScanner.lastIndex);
 
tagScanner.lastIndex = 14;
console.log(tagScanner.exec(tagText));
console.log(tagScanner.lastIndex);

['#PAID', '#SHIPPED', '#DONE'], then #PAID, 5, #SHIPPED, 13, null, then 0.

Sticky matching is less common than global matching, but it matters when you are writing a scanner: each match must start exactly where the previous one ended. If it misses, exec returns null and V8 resets lastIndex to 0.

Checkpoint

Answer all three to mark this lesson complete

Regex gives you precise text cuts when the shape is stable and local. Next, the course turns to time, where the hard part is not matching characters but surviving calendars, time zones, and JavaScript's old Date API.

+50 XP on completion