JavaScript Regular Expressions

5 minute read

A regular expression (regex) is a tiny pattern language for matching, validating, extracting, and replacing text. JavaScript has first-class regex syntax (/pattern/flags) and a complete API on both RegExp and String. This chapter covers the syntax you’ll actually use, all the flags, groups (including named and non-capturing), lookaround, the String methods, and a few real-world patterns.

Two Ways to Create

const a = /hello/i;                    // literal
const b = new RegExp("hello", "i");    // from a string

Use the literal for static patterns and new RegExp(str, flags) when the pattern is built at runtime. With the constructor you must double-escape backslashes — "\\d" for \d.

Quick Tests

/hello/i.test("HELLO World");           // Output: true
"hello world".match(/world/);           // Output: ["world", index: 6, …]
"abc123".replace(/\d+/, "###");         // Output: "abc###"
"a,b;c d".split(/[,;\s]+/);             // Output: ["a", "b", "c", "d"]

Common Character Classes

Token Matches
. any character except line terminators (use s flag for “any”)
\d digit [0-9]
\D non-digit
\w word char [A-Za-z0-9_]
\W non-word
\s whitespace
\S non-whitespace
[abc] one of a, b, c
[^abc] none of a, b, c
[a-z] range
\b word boundary (zero-width)
^ / $ start / end (of input, or line with m flag)

Quantifiers

Token Means
* 0 or more
+ 1 or more
? 0 or 1
{n} exactly n
{n,} at least n
{n,m} n to m
*?, +?, ?? non-greedy (lazy) versions
"<b>hi</b><i>x</i>".match(/<.*>/);     // Output: ["<b>hi</b><i>x</i>"]    greedy
"<b>hi</b><i>x</i>".match(/<.*?>/);    // Output: ["<b>"]                   lazy

Flags

Flag Meaning
g global — find all matches
i case-insensitive
m multiline — ^/$ match line boundaries
s dotall — . matches newlines too
u unicode — full code-point + \p{…}
y sticky — match from lastIndex only
d indices — .indices on each match (ES2022)
const re = /foo/giu;

Tip: Almost always include u for text from users or the web — without it, surrogate-paired characters (emoji, many CJK chars) misbehave.

Capture Groups

const m = "2026-05-16".match(/^(\d{4})-(\d{2})-(\d{2})$/);
m[0];                    // "2026-05-16" (whole match)
m[1]; m[2]; m[3];        // "2026" "05" "16"

Non-capturing group (?:…) when you need grouping but not the value:

"hello!!!".match(/(?:hello){2,}/);     // matches "hellohello" if present

Named capture groups (much more readable):

const m = "2026-05-16".match(/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/);
m.groups.year;           // "2026"
m.groups.month;          // "05"

Backreferences

/(['"])(.*?)\1/.exec(`he said "yes"`);
// matches "yes" surrounded by whatever quote opened it

\1 (or \k<name> for named) refers to a previously captured group.

Lookaround (Zero-Width Assertions)

/\d+(?= USD)/.exec("100 USD");          // "100"   — followed-by
/\d+(?! USD)/.exec("100 EUR");          // "100"   — NOT followed-by
/(?<=\$)\d+/.exec("price $42");         // "42"    — preceded-by
/(?<!\$)\d+/.exec("price 42");          // "42"    — NOT preceded-by

Lookaround doesn’t consume characters — (?=foo) checks for foo ahead, but the cursor stays where it was. Great for pulling values out of a context without including the context in the match.

Anchors and Boundaries

/^\s*$/.test("");                       // empty / whitespace only
/^https?:\/\//.test(url);               // starts with http(s)://
/\.js$/.test("app.js");                 // ends with .js
/\bcat\b/.test("category");             // false — word boundary

Unicode Property Escapes (with u)

/\p{Letter}+/u.test("naïve");           // true
/\p{Emoji}/u.test("hi 👋");              // true
/\p{Script=Greek}+/u.exec("αβγ");       // ["αβγ", …]

Massively more reliable than [A-Za-z] for real-world text.

The String Methods

Method What it does
str.match(re) array of match(es); with g, all matches as strings
str.matchAll(re) iterator of full match objects (needs g)
str.replace(re, repl) first match; replace all if g
str.replaceAll(re, repl) replace all; throws if regex lacks g
str.search(re) index of first match, or -1
str.split(re) split into array
for (const m of "a1 b2 c3".matchAll(/([a-z])(\d)/g)) {
  console.log(m[1], m[2]);              // a 1 / b 2 / c 3
}

Replacement with a Function

const out = "say hello to ada".replace(
  /\b(\w)(\w*)/g,
  (_, first, rest) => first.toUpperCase() + rest
);
// Output: "Say Hello To Ada"

The callback receives (match, p1, p2, …, offset, fullString, groups) — pure power.

Replacement Patterns

In a replacement string:

  • $& — the full match
  • $1, $2 — capture groups
  • $<name> — named groups
  • $\ / $’` — text before / after the match
"John Smith".replace(/(\w+) (\w+)/, "$2 $1");        // "Smith John"
"2026-05-16".replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, "$<d>/$<m>/$<y>");

RegExp Methods

const re = /(\d+)/g;
re.exec("a1 b22 c333");
re.exec("a1 b22 c333");
// Each call advances re.lastIndex — call repeatedly to walk matches

exec + global flag is the low-level loop matchAll wraps for you.

Common Patterns

// Email-ish (good enough for UX, not RFC-perfect)
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

// URL (scheme + host)
const URL_RX = /^https?:\/\/[^\s/]+/i;

// Hex colour
const HEX = /^#(?:[0-9a-f]{3}){1,2}$/i;

// Slug
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

// Strip HTML tags (naive — don't use on untrusted input)
"".replace(/<[^>]+>/g, "");

// Trim each line
text.replace(/^[ \t]+|[ \t]+$/gm, "");

// Camel → kebab
"helloWorldX".replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
// "hello-world-x"

Warning: Email/URL validation with regex is approximate at best. Use platform features (<input type="email">, new URL(str)) for real validation.

Performance and Catastrophic Backtracking

Some patterns explode on certain inputs:

/^(a+)+$/.test("aaaaaaaaaaaaaaaaaaaa!");
// Exponential — modern engines may hang/throw

Rules of thumb:

  • Avoid nested quantifiers on overlapping classes ((a+)+, (.*)*)
  • Anchor (^, $) so the engine doesn’t try every offset
  • Prefer specific classes ([^,]) over . to limit backtracking
  • For untrusted input, set a hard cap on input length
const MD_LINK = /\[(?<text>[^\]]+)\]\((?<url>https?:\/\/[^\s)]+)\)/g;

function extractLinks(md) {
  const out = [];
  for (const m of md.matchAll(MD_LINK)) {
    out.push({ text: m.groups.text, url: m.groups.url });
  }
  return out;
}

extractLinks(`See [docs](https://example.com/d) and [home](https://example.com).`);
// Output:
// [ { text: "docs", url: "https://example.com/d" },
//   { text: "home", url: "https://example.com" } ]

Named groups + matchAll = readable extraction logic.

Common Mistakes

// ❌ Forgetting g with replaceAll
"aaa".replaceAll(/a/, "b");          // TypeError — needs /g

// ❌ Forgetting to escape special chars in dynamic patterns
new RegExp(userInput);               // ReDoS risk + bad matches

// ✅ Escape helper
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
new RegExp(escape(userInput));

// ❌ Reusing /g regex without resetting lastIndex
const re = /x/g;
re.test("xy");                       // true
re.test("xy");                       // false  ← lastIndex carried over

// ❌ Using . without u for emoji-containing strings
/.{1}/u.test("👋");                   // true   (without u: false)

// ❌ Thinking ^/$ are line-based by default
/^line$/.test("a\nline\nb");         // false; add m for multiline

Best Practices Checklist

  • ✅ Prefer named groups (?<name>…) for readability
  • ✅ Add the u flag for any user-facing text
  • ✅ Use matchAll for iterating global matches
  • ✅ Use anchors and specific classes — avoid greedy .* where possible
  • ✅ Escape user-supplied strings before injecting into regex
  • ✅ Use platform validators (URL, <input type="email">) before reaching for regex
  • ❌ Don’t rely on regex for parsing HTML or JSON
  • ❌ Don’t reuse a global regex across calls without minding lastIndex
  • ❌ Don’t write deeply nested quantifiers — beware catastrophic backtracking

What’s Next?

Text wrangling — solved. Next we look at two often-overlooked collection types that beat plain objects and arrays for many jobs: Sets and Maps (plus their weak cousins).

← Previous: JS Modules Next: JS Sets & Maps →

Updated: