Why Your Regex Returns true Then false on the Same Input

javascript dev.to

You write a quick validation loop, run it, and get true, false, true, false for four identical strings. Nothing in the input changed. Nothing in the pattern changed. So what did?

The regex object is carrying state.

The hidden lastIndex property

When a pattern has the g flag, the returned RegExp object keeps a property called lastIndex — the position where the previous match ended. The next .test() or .exec() call starts searching from there instead of from zero. On a successful match lastIndex moves forward; on a failure it resets to 0. Because test() only hands you a boolean, that reset is invisible, and you see just the symptom: the same input reporting different results depending on what came before it.

It bites hardest in exactly the places regex lives longest — a module-level constant, a class field, a cached pattern inside a helper. A regex compiled inline inside a loop behaves fine because it gets thrown away after each pass.

Three ways to fix it

1. Compile fresh each time. A new regex object starts with lastIndex = 0:

function isHexColor(s) {
  return new RegExp('^#[0-9a-f]{6}$', 'i').test(s);
}
Enter fullscreen mode Exit fullscreen mode

2. Reset it manually if you want to keep reusing one object for performance:

const HEX = /^#[0-9a-f]{6}$/i;

function isHexColor(s) {
  HEX.lastIndex = 0;
  return HEX.test(s);
}
Enter fullscreen mode Exit fullscreen mode

3. Use matchAll() when you want every match. It returns an iterator and does not mutate the original pattern, so no index bookkeeping leaks into your logic:

const digits = [...'a1 b2 c3'.matchAll(/[a-z]([0-9])/g)].map(m => m[1]);
Enter fullscreen mode Exit fullscreen mode

The sticky flag too

y (sticky) behaves the same way — it also anchors matching at lastIndex, and it does not reset on failure. If you assemble flag strings dynamically from user input or config, that state can survive far longer than you expect.

Rule of thumb: treat any g- or y-flagged regex as a stateful object. Either create one per use, or set lastIndex = 0 at the top of every call.

💡 When you are chasing this class of bug, seeing every match highlighted as you type beats sprinkling console.log through the file. I keep a regex tester open in a browser tab (there's a free one at codetoolbox.pro/tools/regex-tester.html) — it recompiles the pattern on each keystroke, shows capture groups side by side, and runs entirely in your browser, so nothing you paste in leaves your machine.

Source: dev.to

arrow_back Back to Tutorials