match()
ES3+Retrieves the result of matching a string against a regular expression. With the global flag it returns an array of all matched substrings without capture-group detail; without it, it returns a single exec-style match array carrying the captured groups, the match index, and the input. When nothing matches, it returns null, not an empty array.
Syntax
string.match(regexp)Parameters
regexp RegExp A regular expression object
Return Value
An Array of matches, or null if no match was found
Examples
const str = 'The rain in Spain';
console.log(str.match(/ain/g));
console.log(str.match(/xyz/)); 📌 When to Use
Use match() when you need the matched text itself, not just its existence or position: extracting all hashtags or URLs from a post, pulling the numeric parts out of a formatted string, dissecting a date or URL into components via capture groups, or validating a format while simultaneously harvesting its pieces. The method has two distinct personalities selected by the global flag, and choosing the wrong one is the classic mistake. With the g flag, it returns a plain array of every matched substring, ideal for collect-them-all jobs like hashtag extraction, but it discards capture groups, indices, and named groups. Without g, it stops at the first match and returns the rich exec-style array: element zero is the whole match, subsequent elements are the capture groups, and the groups property exposes named groups, perfect for parsing one structured value. When you need rich detail for every match, neither mode suffices; that is matchAll()'s job, which requires the g flag and yields full match objects lazily. If you only need a boolean, regex test() is cheaper and clearer; if you only need a position, search() or exec() with indices serves better; and if the search text is a literal string, includes() or indexOf() avoids regex machinery entirely. Always handle the null return before touching the result.
⚠️ Common Mistakes
Forgetting that no match returns null: writing text.match(re).length throws a TypeError on clean input, the single most common match() bug. Guard with a null check, default with the nullish idiom match(re) || [], or use optional chaining before touching properties.
Expecting capture groups with the global flag: with g, the result is only the full matched substrings; parentheses in the pattern capture nothing you can see. Parsing all matches with groups requires matchAll() or a manual exec() loop.
Expecting all matches without the global flag: without g, match() stops at the first hit and returns its detail, so code that maps over the 'list of matches' actually iterates over one match and its capture groups, a confusing shape that looks almost right.
Ignoring the shape difference between the two modes: g yields a plain string array, non-g yields a match-array with index, input, and groups properties bolted on. Code written for one shape silently misreads the other, so pin the flag choice next to the code that consumes the result.
Passing a string and assuming it is literal: a non-regex argument is converted with new RegExp(), so '3.14'.match('3.14') also matches '3x14', because the dot became a metacharacter. Escape dynamic text or use literal-string methods for literal searches.
Reading numbered groups after editing the pattern: adding a parenthesized group shifts every later group number, quietly breaking match[2] style indexing. Named groups with the groups property are immune to this and self-documenting.
✅ Best Practices
Default missing results immediately, as in const tags = text.match(re) || [], so downstream code deals with one shape and the null case cannot leak.
Use named capture groups for any pattern with more than one group; match.groups.year reads itself and survives pattern edits that renumber positional groups.
Pick the tool by what you consume: test() for booleans, match() without g for one parse, match() with g for a list of matched substrings, matchAll() for all matches with groups, and replace() when the goal is transformed text.
Keep patterns anchored and specific when validating, using start and end anchors so 'looks like a date somewhere inside' cannot pass for 'is a date'.
Hoist regex literals out of hot loops and reuse them; recompiling patterns per iteration wastes work, and with matchAll() and exec() loops also mind the lastIndex state on shared global regexes.
Validate before trusting groups: even on a successful match, optional groups are undefined, so destructure with defaults or check each piece before converting with parseInt and friends.
⚡ Performance Notes
match() costs whatever the regular expression costs: engine-compiled patterns scan the string once, so simple patterns are linear, but nested quantifiers and heavy alternation can backtrack catastrophically, turning a single call on crafted input into seconds of CPU, a real denial-of-service class (ReDoS) when patterns meet user input. Keep patterns specific, avoid ambiguous nesting, and test them against pathological strings. Each call allocates its result array and match objects, so extracting from many strings in a loop generates garbage; hoist the compiled regex out of the loop, and when you only need existence or position, use test() or search(), which allocate less. For global extraction over large documents, matchAll() is lazy per match, letting you break early, whereas match() with g materializes the whole result array up front. Literal substring jobs are always cheaper through includes() or indexOf() than through the regex engine.
🌍 Real World Example
Data Extraction and Validation
Four extraction-and-validation patterns spanning both of match()'s modes. extractHashtags() uses the global flag to collect every hashtag in a post as a plain array, defaulting null to an empty list, the canonical collect-all idiom. parseUrl() and validateEmail() run anchored, non-global patterns whose positional capture groups split a URL into protocol, host, path, and query, and an email into username, domain, and top-level domain, returning structured objects and failing cleanly on null. parseDate() upgrades to named groups, reading year, month, and day from match.groups, the modern style that keeps group access readable and robust to pattern edits.
// Extract all hashtags from text
function extractHashtags(text) {
const matches = text.match(/#[\w\u0080-\uFFFF]+/g);
return matches || [];
}
console.log(extractHashtags('Hello #world! Learn #JavaScript #coding'));
// ['#world', '#JavaScript', '#coding']
// Parse URL components
function parseUrl(url) {
const pattern = /^(https?:\/\/)?([^\/]+)(\/[^?]*)?(?:\?(.*))?$/;
const match = url.match(pattern);
if (!match) return null;
return {
protocol: match[1] || 'http://',
host: match[2],
path: match[3] || '/',
query: match[4] || ''
};
}
console.log(parseUrl('https://example.com/page?id=123'));
// { protocol: 'https://', host: 'example.com', path: '/page', query: 'id=123' }
// Extract date components using named groups
function parseDate(dateString) {
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = dateString.match(pattern);
if (!match?.groups) return null;
return {
year: parseInt(match.groups.year),
month: parseInt(match.groups.month),
day: parseInt(match.groups.day)
};
}
console.log(parseDate('2024-01-15'));
// { year: 2024, month: 1, day: 15 }
// Validate email format and extract parts
function validateEmail(email) {
const pattern = /^([a-zA-Z0-9._-]+)@([a-zA-Z0-9.-]+)\.([a-zA-Z]{2,})$/;
const match = email.match(pattern);
if (!match) return { valid: false };
return {
valid: true,
username: match[1],
domain: match[2],
tld: match[3]
};
}
console.log(validateEmail('user@example.com'));
// { valid: true, username: 'user', domain: 'example', tld: 'com' }