Spot the bug - #135: Text Parser Log

Why does my vampire detector miss identical counts of bats?

function auditBatCave(roostLog) {
  const batDetector = /bat/g;
  const caveSections = roostLog.split('---');
  const report = [];

  for (const section of caveSections) {
    const batMatches = [];
    let match;
    
    while ((match = batDetector.exec(section)) !== null) {
      batMatches.push(match.index);
    }

    report.push({
      sectionText: section.trim(),
      batCount: batMatches.length,
      positions: batMatches
    });
  }

  return report;
}

const sampleLog = "bat bat bat --- bat bat bat";
console.log(auditBatCave(sampleLog));

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

The bug is that the regex has the g flag, so batDetector keeps its lastIndex between sections.

After the first section finishes, lastIndex is already at the end of that string. When exec() is called on the next section, it starts from that old index and can miss matches.

The simple fix is to reset lastIndex before processing each section:

for (const section of caveSections) {
  batDetector.lastIndex = 0;

  const batMatches = [];
  let match;

  while ((match = batDetector.exec(section)) !== null) {
    batMatches.push(match.index);
  }

  // ...
}

Then both sections correctly report 3 bats.

Nice catch on the g flag behavior. That’s a classic one that bites people. We’ll post the full answer later today.

The g flag always trips people up with exec. It’s a mutable state thing that’s easy to forget.

Interesting

The else if on line 13 is missing a condition. It will always execute.

Spot the Bug answer: The regular expression’s lastIndex property is not reset for each new section.

The fix:
Reset batDetector.lastIndex = 0; at the beginning of each loop iteration for ‘section’.

Why:
When a global regular expression is used in a loop with ‘exec’, its ‘lastIndex’ property updates after each successful match. If the same regex instance is used for multiple strings without resetting ‘lastIndex’, it will start searching from the previous ‘lastIndex’ value, potentially missing matches in subsequent strings or sections.


Got it: @Apexcodes :trophy:

First-answer leaderboard

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

Hmm, yes, the lastIndex property is a classic source of subtle bugs with global regex. It’s like a musician starting a new piece but still in the middle of the previous one.

Look. I’ve seen this exact thing with logs where the parser just keeps going from the last match. You end up missing critical errors because it skipped ahead.

You’ve got it! The g flag on the regex is definitely the culprit here, keeping lastIndex from resetting.

Resetting batDetector.lastIndex = 0; inside the loop for each section is the way to go.