Ch. 1 · JavaScript

JavaScript RegExp Stateful Flags and lastIndex

Why a global or sticky RegExp can alternate between matching and failing, and how lastIndex and the g/y flags interact with test and exec.

~2 min readintermediateupdated Oct 5, 2026

A regular expression with the g (global) or y (sticky) flag keeps a mutable lastIndex on the RegExp object itself. Each call to .test() or .exec() advances it, so reusing that object makes the same input match, fail, then match again. The bug is state on the pattern, not the string.

Before you start

You should be comfortable with regular expression syntax and string methods such as match and replace. This article focuses on the stateful flags; it assumes basic pattern knowledge.

Step-by-step walkthrough

Step 1: Understand what lastIndex tracks

Without g or y, .exec always starts at position 0 and lastIndex is ignored. With g or y, .exec starts at lastIndex, moves it past the match on success, and resets it to 0 when it fails. .test behaves the same way under the hood.

Step 2: Do not share a stateful RegExp

A module-level /pattern/g reused across calls carries state between them. Because .test resets lastIndex to 0 only on failure, a second call on the same string can return the opposite of the first. Create a fresh literal per call, or reset re.lastIndex = 0 deliberately.

Step 3: Choose the right method

For a boolean “does this string contain the pattern”, prefer String.prototype.includes or a non-global .test. Reserve g for iterating all matches with matchAll or a loop on .exec, and y for anchored, contiguous matching from a position.

Worked scenario

Run this with Node.js. The two identical calls return different results because the pattern remembers where it stopped.

const re = /a/g;
console.log(re.test('a')); // true, lastIndex is now 1
console.log(re.test('a')); // false, lastIndex reset to 0
console.log(re.lastIndex); // 0
JavaScript

Walk through the example

The first .test matches at index 0, advances lastIndex to 1, and returns true. The second call starts searching at index 1, finds nothing, resets lastIndex to 0, and returns false. A third call would return true again. The pattern is not broken; it is carrying iteration state that a one-off membership test should not use.

Common mistake

Storing a global regex in a constant and calling .test in a loop or across requests, producing intermittent, input-order-dependent failures that are hard to reproduce. Another mistake is assuming String.prototype.match with g respects lastIndex: it ignores it and returns all matches.

Verify the behavior

Call .test twice on the same string with a shared g regex and assert the results differ. Create the regex inside the function and assert both calls return true. For iteration, use matchAll and assert it returns every match regardless of prior state, since it clones the pattern internally.

Interview exercise

A validation helper intermittently rejects valid input. It uses const EMAIL = /.../g and EMAIL.test(value). What is wrong?

Answer and reasoning

The shared g regex remembers lastIndex between calls, so whether a value matches depends on the previous call. Validation is a membership test, so it should use a non-global pattern, which ignores lastIndex entirely, or a fresh regex created per call. If global iteration is genuinely needed, reset lastIndex = 0 before each independent scan.

Continue learning

Review the stateful-flag behavior in the MDN RegExp lastIndex reference, compare string handling in JavaScript equality and coercion, and continue with the JavaScript interview questions.

More in JavaScript

read ✓JavaScript · mid

JavaScript Date and Timezone Pitfalls

Parse and format dates without timezone surprises: zero-based months, date-only versus date-time parsing, and why UTC storage avoids drift.

~3 min readread →
read ✓JavaScript · mid

JavaScript Number Precision and BigInt

Why 0.1 + 0.2 is not 0.3, where safe integer range ends, and when BigInt is the right tool for large identifiers and money.

~2 min readread →
esc