Check Whether a String Matches a Regex in JS —test() vs exec() vs match()

Every JavaScript developer needs to answer this question:

Does this string match the pattern?

The answer seems straightforward. But JavaScript provides three different methods to check regex matches, and each behaves differently in subtle ways.

The wrong choice can lead to:

  • false negatives (you miss a valid match)
  • false positives (you accept an invalid string)
  • state leaking across calls
  • performance problems

This guide explains exactly how test(), exec(), and match() differ, when to use each, and how to avoid the most common bugs.

If you want to experiment with these methods interactively, the Regex Tester helps visualize matches in real time.


Method 1: regex.test(string)

The simplest method. Returns true or false.

const regex = /\d+/;
console.log(regex.test("abc123")); // true
console.log(regex.test("abc"));    // false

test() is:

  • the fastest method
  • the most readable for boolean checks
  • the most commonly used

Always use test() when you only need a yes/no answer.


The lastIndex Trap with test()

Here is where developers get burned.

With a global regex (g flag), test() updates lastIndex:

const regex = /\d+/g;

console.log(regex.test("abc 123")); // true
console.log(regex.test("abc 123")); // false —what?
console.log(regex.test("abc 123")); // true

Every other call returns false because lastIndex advances past the match and does not reset.

This is one of the most common JavaScript regex bugs in production.


Why This Happens

When test() finds a match, it sets regex.lastIndex to the position after the match. The next test() call starts from that position, not from the beginning.

const regex = /\d+/g;
const text = "abc 123 def 456";

console.log(regex.lastIndex); // 0
regex.test(text);
console.log(regex.lastIndex); // 7 (after "123")
regex.test(text);
console.log(regex.lastIndex); // 15 (after "456")
regex.test(text);
console.log(regex.lastIndex); // 0 (no match, resets)

How to Avoid the Trap

Option 1: Avoid the g flag with test()

const regex = /\d+/; // No g flag
console.log(regex.test("abc 123")); // true
console.log(regex.test("abc 123")); // true —consistent

Option 2: Reset lastIndex

const regex = /\d+/g;
regex.lastIndex = 0;
console.log(regex.test("abc 123")); // true

Option 3: Use match() instead

const hasMatch = "abc 123".match(/\d+/g) !== null;

Method 2: regex.exec(string)

Returns a match object with groups, or null.

const regex = /(\d+)/;
const result = regex.exec("abc 123");

console.log(result[0]); // "123"
console.log(result[1]); // "123"
console.log(result.index); // 4

For boolean checks, you can use exec():

if (regex.exec("abc 123") !== null) {
  // Match found
}

But test() is better for this —it is faster and more concise.


When to Use exec() for Matching

Use exec() when you need both:

  • a match check
  • access to captured groups
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const result = regex.exec("Today is 2026-06-13");

if (result) {
  console.log("Found date:", result[0]);
  console.log("Year:", result[1]);
  console.log("Month:", result[2]);
  console.log("Day:", result[3]);
}

exec() Has the Same lastIndex Trap

const regex = /\d+/g;
const text = "abc 123";

console.log(regex.exec(text)); // ["123"]
console.log(regex.exec(text)); // null —lastIndex advanced past end

Same issue as test(). Same solutions apply.


Method 3: string.match(regex)

Returns:

  • array of matches (with g flag)
  • array with full match and groups (without g flag)
  • null if no match
// Without g —returns groups
const result = "abc 123".match(/(\d+)/);
console.log(result[0]); // "123"
console.log(result[1]); // "123"

// With g —returns all full matches
const result2 = "abc 123 def 456".match(/\d+/g);
console.log(result2); // ["123", "456"]

For boolean checking, use:

if ("abc 123".match(/\d+/)) {
  // Match found
}

But test() is still better for pure boolean checks.


Performance Comparison

const regex = /\d+/;
const text = "abc 123 def 456 ghi 789";

// test() —fastest
console.time("test");
for (let i = 0; i < 100000; i++) {
  regex.test(text);
}
console.timeEnd("test");

// exec() —medium
console.time("exec");
for (let i = 0; i < 100000; i++) {
  regex.exec(text);
}
console.timeEnd("exec");

// match() —slowest (allocates array)
console.time("match");
for (let i = 0; i < 100000; i++) {
  text.match(regex);
}
console.timeEnd("match");

Results (Node.js 20, approximate):

test:   ~3ms
exec:   ~5ms
match:  ~6ms

test() is consistently the fastest because it returns a boolean without allocating match objects.

Related reading: \d Is Less Efficient Than [0-9] —Node.js Regex Performance Deep Dive


Decision Table

NeedBest MethodNotes
Yes/no match checktest()Fastest, most readable
Match check + groupsexec()Returns groups, single match
All matches (no groups)match() with gReturns array of strings
All matches + groupsmatchAll()ES2020+, requires g flag

Real-World Example: Form Validation

function validateEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

// test() is perfect here —boolean, no groups needed
if (!validateEmail(input)) {
  showError("Invalid email");
}

Related reading: Best Regex for Email Validation in JavaScript


Real-World Example: Extracting Data After Checking

const text = "Order #12345: $99.99";
const hasOrder = /Order #\d+/.test(text);

if (hasOrder) {
  // Now extract details
  const regex = /Order #(?<id>\d+): \$(?<amount>[\d.]+)/;
  const match = regex.exec(text);
  console.log(match.groups.id);     // "12345"
  console.log(match.groups.amount); // "99.99"
}

This runs the regex twice. For most code, that is fine. For hot paths, combine into one exec() call.


Real-World Example: Checking Multiple Strings

const inputs = ["abc", "123", "abc123", "!@#"];
const regex = /\d+/;

inputs.forEach(input => {
  // test() is clean and fast for multiple checks
  console.log(`${input}: ${regex.test(input)}`);
});

Since there is no g flag, test() is safe and predictable.


Common Mistake: Reusing Global Regex with test()

const regex = /\d+/g;  // g flag!

["abc123", "def456", "ghi789"].forEach(str => {
  console.log(regex.test(str));
});
// true, true, false —wrong!

The third string returns false because lastIndex is advanced.

Fix: remove the g flag or reset lastIndex.


Common Mistake: Using match() When You Only Need a Boolean

// Overkill
if (str.match(/^\d+$/)) {
  // ...
}

// Better
if (/^\d+$/.test(str)) {
  // ...
}

match() allocates an array or null. test() returns a boolean. For pure checking, test() is always the right choice.


Common Mistake: Assuming match() Returns Null

Both match() and exec() return null when there is no match:

const result = "abc".match(/\d/);
console.log(result); // null

if (result) {
  // Only reachable if match exists
}

This is correct. But if you access result[0] without checking for null, you get a TypeError.


The startsWith, endsWith, includes Alternative

Sometimes you do not need regex at all:

// Instead of:
/^hello/.test(str)
// Use:
str.startsWith("hello")

// Instead of:
/world$/.test(str)
// Use:
str.endsWith("world")

// Instead of:
/foo/.test(str)
// Use:
str.includes("foo")

String methods are faster and more readable when the pattern is simple.

Related reading: Regex Works in Regex101 but Not in JavaScript —Why


FAQ

What is the fastest way to check if a string matches a regex in JavaScript?

regex.test(string) is the fastest method for boolean checks.


What is the difference between test() and exec()?

test() returns true or false. exec() returns a match object with groups or null.


Why does test() return alternating true/false?

This happens with the g flag. test() updates lastIndex, causing the next call to start from a different position.


Should I use test() or match() for validation?

Use test() for validation. It is faster and returns a boolean directly.


Does exec() have the lastIndex issue?

Yes. exec() also updates lastIndex with the g flag.


Can I use match() without groups for a boolean check?

Yes, but test() is more efficient.


Final Thoughts

Three methods for one task. The choice depends on what you need:

  • Just a yes/no? Use test(). It is faster, simpler, and the intent is clear.
  • Need groups too? Use exec() for single matches, matchAll() for multiple.
  • Need all matches as strings? Use match() with the g flag.

The lastIndex trap with test() and exec() catches almost everyone at least once. Once you know it exists, the fix is trivial —avoid g with test(), or reset lastIndex.

And if you want to see exactly how each method behaves with different patterns, the Regex Tester visualizes matches, groups, and positions clearly.

You may also find these related developer tools useful: