Lesson 40 of 80
Regex syntax is mostly the same everywhere, but JS's flags and its match methods have their own personality — and the classic /g gotcha has broken real production code.
16 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A regular expression is a pattern for matching text. The core syntax — character classes, quantifiers, groups — is close to universal across languages, so this lesson focuses on the JS-specific part: the flags that change how a pattern behaves, and the five different methods JS gives you to actually run one, each suited to a different job.
A regex literal looks like /pattern/flags. The flags that matter most: g (global — find all matches, not just the first), i (case-insensitive), m (multiline — ^/$ match line boundaries, not just string boundaries), and s (dotAll — . matches newlines too, which it normally doesn't).
A simple but real email-shape check (deliberately not RFC-complete — full email validation is famously not worth doing with regex alone):
const emailPattern = /^[\w.+-]+@[\w-]+\.[a-z]{2,}$/i;
emailPattern.test("dee@example.com"); // true
emailPattern.test("not an email"); // false
test() just answers yes/no — good for validation, nothing else.
For extracting the actual matched text, match() and matchAll() do the work:
const text = "Contact dee@example.com or sam@test.org";
const emailFinder = /[\w.+-]+@[\w-]+\.[a-z]{2,}/gi;
text.match(emailFinder);
// ["dee@example.com", "sam@test.org"]
[...text.matchAll(emailFinder)].map(m => m[0]);
// ["dee@example.com", "sam@test.org"] -- but matchAll gives full match objects with groups + index
replace() is the one most people use daily, and it accepts a function for computed replacements:
"2026-09-09".replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1");
// "09/09/2026" -- $1, $2, $3 refer to the capture groups
/g is stateful — lastIndex persists across calls. Calling .exec() or .test() repeatedly on the same global regex object advances an internal lastIndex pointer, so each call resumes from where the last one left off instead of starting fresh. Reuse a g-flagged regex across unrelated calls without resetting lastIndex (or without creating a new regex), and test() starts silently returning false on inputs that should match, or exec() starts skipping matches.test() and exec() behave differently with and without g. Without g, .exec() always returns the first match and doesn't advance anything. With g, it becomes a "call this in a loop" API that returns the next match each time, and null once it's exhausted the string — this is the actual intended way to iterate matches with exec(), but it trips people who don't expect the statefulness.^ and $ mean "whole string" unless m is set. Without the multiline flag, ^/$ anchor to the very start/end of the entire string, not each line — a common surprise when validating multi-line input and expecting per-line anchoring.Regex is a small standalone pattern language embedded inside JS, similar in spirit to how JSON is a data-format language embedded via JSON.parse/stringify — both hand a chunk of text to a separate, tightly-scoped interpreter rather than solving the problem in JS syntax itself. The lastIndex statefulness gotcha is worth connecting to closures: a global-flagged regex object is mutable state that any code holding a reference to it can accidentally advance, the same class of bug as an unexpectedly shared mutable value anywhere else.