Meet Modi
Back to Blog
·5 min

The fallback branch that copied math it didn't understand

A results report marked three question types as wrong, every time, no exceptions. The bug: one branch subtracted 1 from an index. The fallback copied that subtraction without asking why it was there.

By Meet Modi
Data MappingTestingFrontend

A quiz results report started marking answers wrong that were right. Not randomly. Every text-based question, every match-pairs question, every image-based question, on every attempt, for every user.

Single-choice and true/false questions scored fine. That was the clue nobody followed up on for two release cycles.

The bug: one code branch subtracted 1 from an index the backend sent. The fallback branch copied that subtraction. The backend never sent a 1-based index for anything.

Take 1 (wrong): copy the working branch's math

The mapping function had grown by accretion. Single-choice and true/false came first, and someone had found, through trial and error, that subtracting 1 from the backend's index made the frontend model line up:

function mapAnswerIndex(question: BackendQuestion): number {
  switch (question.type) {
    case "single_choice":
    case "true_false":
      return question.correctIndex - 1;
    default:
      // handles text, match_pairs, image_based
      return question.correctIndex - 1;
  }
}

The default branch was written by someone who saw a working pattern and extended it to cover the types nobody had gotten around to testing yet. It compiles. It matches the style of the branch above it. It's wrong for every type it's supposed to handle.

Here's the mechanical failure: a match-pairs question comes back with correctIndex: 2, meaning the third pair, zero-based. The fallback returns 1. The frontend model now points at the second pair. The user picked the third pair, correctly, and the report marks it wrong.

Single-choice and true/false only ever have two or four options. Subtracting 1 from a small zero-based index sometimes lands on another valid index by accident, and for the specific test cases someone happened to try, it looked plausible enough to ship.

Take 2 (right): read the actual contract

I went to the backend's serializer instead of the frontend's git history. Every question type, no exceptions, emits a zero-based index. There was never a 1-based variant. The subtraction in the first branch wasn't compensating for a real difference, it was compensating for a bug that predated it, in some earlier version of the client, that had since been fixed elsewhere and never cleaned up here.

function mapAnswerIndex(question: BackendQuestion): number {
  // Backend always sends zero-based indices, regardless of question type.
  // Do not subtract. See INDEX_CONTRACT.md.
  return question.correctIndex;
}

One line, no switch, no special case. The fix was smaller than the bug.

I added a regression suite that asserts the invariant directly: for a fixture of every question type the backend can send, the mapped index equals the raw index, unchanged. Not a snapshot test that happens to catch this one bug, a test that states the rule so the next person editing this function has to actively break an assertion to reintroduce it.

What I'd do differently

I would have asked why the subtraction existed before extending it to a new branch. Nobody did, including me, until enough support tickets piled up with the same shape: real users on the same three question types, always wrong, never a single-choice question in the batch.

The tell was right there in the ticket pattern. I just read it as three separate reports instead of one report split three ways.

More Posts