JavaScript Regex

A regular expression (regex) is a pattern used to match, search, extract, or replace text. JavaScript has regex support built directly into the language through the RegExp type, and you’ll use it constantly: validating form input, parsing log lines, pulling structured data out of strings, or doing search-and-replace that goes far beyond a simple substring check. This lesson covers the full syntax, the methods that use regex, how the matching engine works under the hood, and the mistakes that trip up almost everyone the first time.

Overview: How Regular Expressions Work

A regular expression is a compact mini-language for describing a set of strings. Instead of writing a loop that manually inspects each character, you write a pattern and hand it to JavaScript’s regex engine, which does the character-by-character matching for you.

In JavaScript, a regex is represented by a RegExp object. You can create one two ways: a regex literal written between slashes, like /ab+c/, or the RegExp constructor, new RegExp("ab+c"). Literals are parsed once when the script is compiled, before that line of code even executes, so reusing the same literal in a loop is efficient because the engine doesn’t recompile it every time. The constructor form parses its pattern string at runtime, which makes it the only way to build a pattern dynamically — for example, from a variable or user input.

Internally, most JavaScript engines (V8’s regex engine is called Irregexp) compile a pattern into a small matching program, similar in spirit to how a compiler turns source code into instructions. When you call a matching method, the engine walks the input string trying to align the pattern against it, character by character, using backtracking to explore alternative ways the pattern could match when an attempt fails partway through. This backtracking behavior is central to how regex feels intuitive most of the time but can occasionally behave in surprising or slow ways — more on that in the “Under the Hood” section below.

A regex object carries flags that change how matching behaves (case sensitivity, whether it scans the whole string for multiple matches, and so on), and it exposes methods like test() and exec(). Strings also have regex-aware methods — match(), matchAll(), replace(), replaceAll(), search(), and split() — that accept a RegExp as an argument.

Syntax

const pattern1 = /hello/i;
const pattern2 = new RegExp("hello", "i");

console.log(pattern1.test("Hello World")); // true
console.log(pattern2.test("Hello World")); // true

The general form is /pattern/flags for a literal, or new RegExp("pattern", "flags") for the constructor. Here’s what each flag does:

Flag Name Effect
g global Find all matches instead of stopping at the first one
i ignoreCase Case-insensitive matching
m multiline ^ and $ match the start/end of each line, not just the whole string
s dotAll . also matches newline characters
u unicode Treats the pattern and string as full Unicode code points
y sticky Matches only starting exactly at lastIndex, with no scanning ahead
d hasIndices Records the start/end index of each captured group

Inside the pattern itself, certain characters are metacharacters with special meaning:

Symbol Meaning
. Any character except newline (unless s flag is set)
^ / $ Start / end of string (or line, with m)
* / + / ? Zero-or-more, one-or-more, zero-or-one of the preceding token
{n,m} Between n and m repetitions
[...] Character class — matches any one character listed
(...) Capturing group
(?:...) Non-capturing group
(?<name>...) Named capturing group
| Alternation (OR)
\d \w \s Digit, word character, whitespace (uppercase versions negate)
\b Word boundary

Examples

Example 1: Testing, Matching, and Searching

const str = "The rain in Spain falls mainly on the plain.";

console.log(/ain/.test(str));

const matches = str.match(/ain/g);
console.log(matches);

console.log(str.search(/spain/i));

Output:

true
[ 'ain', 'ain', 'ain', 'ain' ]
12

test() returns a plain boolean — useful for validation. match() with the g flag returns every matched substring as an array. search() works like indexOf() but accepts a regex, so here it finds “Spain” case-insensitively at index 12, even though the pattern was written in lowercase.

Example 2: Capture Groups and Replace

const date = "2026-07-19";
const isoPattern = /(\d{4})-(\d{2})-(\d{2})/;

const formatted = date.replace(isoPattern, "$2/$3/$1");
console.log(formatted);

const [, year, month, day] = date.match(isoPattern);
console.log(`Year: ${year}, Month: ${month}, Day: ${day}`);

Output:

07/19/2026
Year: 2026, Month: 07, Day: 19

The parentheses around \d{4}, \d{2}, and \d{2} create three capture groups. In replace(), $1, $2, and $3 refer back to those groups, letting you rearrange the matched text. match() on a non-global pattern returns an array where index 0 is the full match and indices 1+ are the captured groups, which is why the destructuring skips the first element.

Example 3: Named Groups and a Global Exec Loop

const text = "Contact: alice@example.com, bob@test.org";
const emailPattern = /(?<user>[\w.+-]+)@(?<domain>[\w.-]+\.\w+)/g;

let match;
while ((match = emailPattern.exec(text)) !== null) {
  console.log(`${match.groups.user} at ${match.groups.domain}`);
}

Output:

alice at example.com
bob at test.org

Named groups ((?<name>...)) let you access captures by a readable label via match.groups instead of numeric index. With the g flag, calling exec() repeatedly on the same regex object advances its internal lastIndex after each match, letting you loop over every match in the string until exec() returns null.

Under the Hood: Backtracking

JavaScript’s regex engine is a backtracking matcher. It tries to match the pattern starting at each position in the string, and when a quantifier like * or + is involved, it first grabs as much as it can (this is called greedy matching), then gives characters back one at a time if the rest of the pattern fails to match, until it finds a way to succeed or exhausts every possibility.

const str2 = "a123b456b";
const greedyMatch = str2.match(/a.*b/)[0];
const lazyMatch = str2.match(/a.*?b/)[0];

console.log(greedyMatch);
console.log(lazyMatch);

Output:

a123b456b
a123b

.* is greedy: it first consumes the entire rest of the string, then backtracks character by character until it finds a trailing b, landing on the last one. Adding ? after a quantifier (*?, +?) makes it lazy: it consumes as little as possible, so it stops at the very first b it finds. Understanding greedy versus lazy matching is essential for writing patterns that grab exactly the substring you intend, especially around delimiters like quotes, tags, or brackets.

Common Mistakes

Mistake 1: The g Flag and the lastIndex Trap

A regex object with the g (or y) flag remembers where it left off between calls, via its lastIndex property. Reusing the same global regex for repeated test() or exec() calls produces results that depend on call order, which surprises almost everyone the first time.

const pattern = /foo/g;

console.log(pattern.test("foo"));
console.log(pattern.test("foo"));
console.log(pattern.test("foo"));

Output:

true
false
true

The first call matches and moves lastIndex to 3 (the end of the match). The second call starts searching from index 3, finds nothing left to match, returns false, and resets lastIndex back to 0 as a side effect of the failed search. The third call then starts over from 0 and matches again. The fix: don’t reuse a global regex across independent test() calls — use a non-global pattern for simple checks, or explicitly reset pattern.lastIndex = 0 before each check if you must reuse the object.

Mistake 2: Forgetting to Escape Special Characters

Characters like ., *, +, ?, (, and [ have special meaning in a regex. Writing them literally without escaping produces a pattern that’s far more permissive than intended.

const wrongPattern = /3.14/;
console.log(wrongPattern.test("3x14"));

const correctPattern = /3\.14/;
console.log(correctPattern.test("3x14"));
console.log(correctPattern.test("3.14"));

Output:

true
false
true

In the first pattern, the unescaped . means “any character,” so it happily matches "3x14" — not what was intended if the goal was to match the literal decimal number 3.14. Escaping it with a backslash, \., makes it match only an actual period.

Best Practices

  • Prefer regex literals (/pattern/) over the RegExp constructor unless you need to build the pattern dynamically from a variable.
  • Use named capture groups ((?<name>...)) for patterns with several captures — numeric indices get unreadable and fragile as the pattern grows.
  • Use matchAll() instead of a manual exec() loop when you just need all matches at once — it returns an iterator and avoids the lastIndex footgun entirely.
  • Use replaceAll() (not replace()) when you intend to replace every occurrence with a global regex or plain string, since replace() without the g flag only replaces the first match.
  • Always escape user-supplied strings before interpolating them into a regex (escape characters like . * + ? ( ) [ ] { } ^ $ |) to avoid unintended pattern behavior.
  • Keep patterns readable: break complex regexes into named variables or comments explaining each part, since dense regex is notoriously hard to re-read months later.
  • Avoid deeply nested quantifiers (like (a+)+) on untrusted input — certain patterns can trigger catastrophic backtracking, where matching time grows exponentially with input length.
  • Test edge cases explicitly: empty strings, strings with only whitespace, and strings with unexpected Unicode characters.

Practice Exercises

  • Write a regex that validates a simple username: 3–16 characters, letters, digits, and underscores only. Test it against "user_01" (should pass) and "ab" (should fail, too short).
  • Given the string "cat, hat, bat, mat", write a regex with the g flag that extracts every word ending in "at", and use match() to log the resulting array.
  • Use a capture group and replace() to convert the string "John Smith" into "Smith, John" (hint: capture the first and last name separately and reorder them in the replacement string).

Summary

  • A regex describes a pattern of text; JavaScript represents it as a RegExp object, created via a literal /pattern/flags or new RegExp("pattern", "flags").
  • Flags like g, i, and m change matching behavior; metacharacters like ., *, (), and [] give the pattern its structure.
  • The engine matches using backtracking, trying greedy matches first and giving characters back on failure; lazy quantifiers (*?, +?) reverse that preference.
  • Key methods: test() and exec() on RegExp; match(), matchAll(), replace(), replaceAll(), search(), and split() on strings.
  • Global (g) regex objects carry state via lastIndex between calls — a common source of subtle bugs.
  • Always escape special characters and user input, and watch for catastrophic backtracking in complex nested patterns.