Shared Global Regular Expression Matched Repeatedly

ID

javascript.regex_global_flag_reuse

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

JavaScript

Tags

regex, reliability

Description

Reports a regular expression carrying the g or y flag that is stored in a variable and then matched more than once with test or exec. Those two methods are the only ones that keep state: each call starts at the object’s lastIndex and writes it back, so the same subject alternates between matching and not matching on consecutive calls.

Two shapes are reported: a shared global regex matched at two or more call sites, and a single test call reached repeatedly — one inside a loop or a nested function declared outside it, which is the filter / some idiom and the most common way the defect ships.

A while m = RE.exec(s) loop is the language’s own way to walk every match and relies on lastIndex advancing, so a lone exec is never reported. Writing RE.lastIndex anywhere silences the regex entirely: the author is managing the cursor on purpose. The other String methods are stateless with a global regex — replace and split restart from the beginning, match and matchAll ignore the cursor — so a regex only handed to those is left alone.

Rationale

// Bad
const HAS_DIGIT = /\d/g;

HAS_DIGIT.test("a1");   // true  -- lastIndex is now 2
HAS_DIGIT.test("a1");   // false -- searched from position 2
HAS_DIGIT.test("a1");   // true  -- lastIndex was reset by the failure

const withLetter = names.filter((n) => HAS_DIGIT.test(n));  // drops every other match
  • The regex object carries a cursor. Hoisting it to module scope, which is the usual way to avoid recompiling a pattern, is exactly what makes the cursor shared.

  • The result depends on how many times the function has been called before, so the bug is order-dependent: it passes in isolation and fails in a suite, or the other way round.

  • In a filter or a some the effect is a silent halving of the matches, with no error anywhere.

Remediation

Drop the flag when the call only asks whether the subject matches — g changes nothing for test except the cursor. If the flag is needed for another use of the same pattern, build the regex where it is matched, or reset the cursor explicitly.

// Good
const HAS_DIGIT = /\d/;                  // no cursor to carry
HAS_DIGIT.test("a1");                    // true, every time

const withLetter = names.filter((n) => /\d/g.test(n));   // a fresh object per call

MATCHER.lastIndex = 0;                   // deliberate, and now visible
MATCHER.test(subject);

© 2026 Xygeni. All rights reserved