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); // 0Walk 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.