Spot the bug - #167: Member Badge Verifier

Why does this badge validator alternate between valid and invalid?

const BADGE_PATTERNS = {
  scout: /^SCOUT-[A-Z]{3}-\d{4}$/g,
  ranger: /^RANGER-[A-Z]{2}-\d{3}$/g,
  pilot: /^PILOT-\d{4}-[A-Z]{2}$/g
};

function validateRosterBadges(roster) {
  const results = [];

  for (const member of roster) {
    const pattern = BADGE_PATTERNS[member.role];
    if (!pattern) {
      results.push({
        id: member.id,
        role: member.role,
        badge: member.badge,
        status: 'UNKNOWN_ROLE'
      });
      continue;
    }

    const isValid = pattern.test(member.badge);
    results.push({
      id: member.id,
      role: member.role,
      badge: member.badge,
      status: isValid ? 'APPROVED' : 'REJECTED'
    });
  }

  return results;
}

const squad = [
  { id: 101, role: 'scout', badge: 'SCOUT-ABC-1234' },
  { id: 102, role: 'scout', badge: 'SCOUT-XYZ-9876' },
  { id: 103, role: 'scout', badge: 'SCOUT-DEF-4567' }
];

console.log(validateRosterBadges(squad));

Reply with what is broken and how you would fix it.

That g flag on the regex patterns is the problem.

It keeps track of the last match, so subsequent .test() calls start from where the last one left off. You can fix it by removing the g flag from all the regex patterns in BADGE_PATTERNS. Or, if you need the global flag for other reasons, you could reset pattern.lastIndex = 0 before each pattern.test() call.

Spot the Bug answer: The regular expressions use the global flag ‘g’, which causes the ‘test’ method to maintain state across calls.

The fix:
Remove the ‘g’ flag from all regular expressions in BADGE_PATTERNS.

Why:
When a regex with the ‘g’ flag is used with ‘test’, its ‘lastIndex’ property is updated after each match. Subsequent calls to ‘test’ will start searching from ‘lastIndex’, leading to alternating results (true, false, true, false) if the string matches, because it will find a match, then not find one from the new ‘lastIndex’, then reset and find one again.

First-answer leaderboard

  1. @emmawalter5 - 9 (firsts) :trophy:
  2. @kirupa - 8 (firsts)
  3. @Apexcodes - 6 (firsts)
  4. @adnanahmed - 2 (firsts)

That lastIndex behavior with the g flag is such a classic edge case. It’s so subtle, especially if you’re not expecting it.

I just avoid g with test() entirely because of this. String.prototype.matchAll() is my go-to if I need to find all occurrences and still get the capture groups.

You’re right, lastIndex with the g flag on test() can definitely be a tricky one. It’s easy to forget it’s stateful.

Here’s a tutorial on regular expressions in JavaScript that might help explain why that happens: https://www.kirupa.com/html5/regular_expressions_js.htm