DEV Community

Cover image for Why the Same Bug Keeps Coming Back - And How to Stop It
Deepanshu Kumar
Deepanshu Kumar

Posted on

Why the Same Bug Keeps Coming Back - And How to Stop It

A few months ago, I fixed a bug on a Friday afternoon. Status filter on a dashboard widget was returning empty results for some users. Found the problem, added one line, tests passed, shipped it. Felt good about it.

The following Tuesday, someone filed another ticket. Different widget, same symptom. I looked at the code and felt that specific kind of embarrassment you get when you realize the answer was right in front of you the whole time.

I hadn't fixed the bug. I fixed one place where the bug lived. The same broken pattern was sitting in three other handlers, and I hadn't thought to check.


What actually happened

The app was a multi-tenant SaaS where users have permissions scoped to specific warehouses. Dashboard widgets query orders, but only the ones the user is allowed to see. Simple enough.

We had six of these query handlers. Someone had written the first one correctly, copy-pasted it five times as new widgets were added, and somewhere along the way one important line kept getting dropped.

// Priority widget handler — written correctly
public async Task<List<Priority>> Handle(GetPrioritiesQuery query)
{
    var permittedWarehouses = await _warehouseService.GetPermittedWarehouseIds(query.UserId);
    permittedWarehouses.Add(null); // include orders with no warehouse assigned

    return await _repository.GetPriorities(permittedWarehouses);
}

// Status widget handler — the one that broke
public async Task<List<Status>> Handle(GetStatusesQuery query)
{
    var permittedWarehouses = await _warehouseService.GetPermittedWarehouseIds(query.UserId);
    // that null append never made it here

    return await _repository.GetStatuses(permittedWarehouses);
}
Enter fullscreen mode Exit fullscreen mode

The null append matters because some orders aren't assigned to any warehouse. Without it, those orders get filtered out entirely. For users whose entire order history happened to be unassigned, the widget showed nothing at all.

One line. Easy fix. But I only fixed it in the one handler that had broken visibly.

When I finally audited all six:

GetOrderStatuses     → ❌ missing null append
GetOrderPriorities   → ✅ has null append  
GetOrderAssignees    → ❌ missing null append
GetOrderLocations    → ✅ has null append
GetOrderCategories   → ❌ missing null append
GetOrderCosts        → ✅ has null append
Enter fullscreen mode Exit fullscreen mode

Three broken, three fine. The broken ones just hadn't surfaced yet, either because fewer users hit those widgets or because the affected users had quietly assumed the data wasn't there and moved on without filing a ticket.


Why this keeps happening

The code didn't start inconsistent. Someone wrote the first handler carefully. The inconsistency crept in through copy-paste, through deadline pressure, through the very reasonable assumption that "this is basically the same as that other handler." Nobody set out to leave three bugs behind.

The real trap is how bugs announce themselves. They don't show up as patterns. They show up as individual tickets, individual screens, individual complaints. Your natural instinct is to find the broken thing and fix it. That instinct is mostly right, but it misses a step: asking whether the broken thing is one instance of a broader pattern, or the pattern itself.

In my case the pattern was "handlers that filter by warehouse permission don't consistently handle unassigned orders." I fixed one handler. I should have fixed the category.


The habit that changes this

Before writing the fix, write down what's wrong in plain language. Not which file, not which method. The actual rule that got violated.

Weak: "GetOrderStatuses doesn't append null to the warehouse list"

Better: "Warehouse-permission queries don't consistently include null to catch unassigned orders"

The second version is searchable. It tells you what to look for everywhere else, not just in the file you already have open.

Then before merging, run the search:

grep -r "GetPermittedWarehouseIds" --include="*.cs" .
Enter fullscreen mode Exit fullscreen mode

Read every result. For each one ask: should this also have the fix? If yes, add it to the same PR. Not a follow-up ticket, not a "I'll get to it," the same PR.

I know that feels like scope creep. It isn't. The one-line fix takes five minutes. The audit takes twenty. Skipping the audit trades twenty minutes now for two more Tuesday tickets later.


A way to think about it

Imagine a building with fire suppression systems. You find one blocked sprinkler head. The right move is not to unblock it and sign off on the inspection. You check every head on that floor, because blocked sprinklers don't usually happen in isolation.

The fix you wrote is the unblocked head. The audit is the inspection. Both are part of the job, and most engineers are very good at the first one and inconsistent about the second.


Before you close the PR

Three questions, in order:

  1. What is the pattern in plain English? Not the file or method name, the rule that was broken.
  2. Where else does this pattern appear? Search for it. Do not assume you already know.
  3. Are all of those places now consistent? Fix them in this PR or file the follow-up tickets before the browser tab closes, while the context is still fresh.

The third question is the one that gets skipped. It got skipped by me on that Friday, and it cost a Tuesday.


Bugs are rarely one-offs. The one that got reported was just the one someone noticed. The rest are waiting for the right user, the right data, the right edge case to make them visible.

Fix the pattern. Not just the line.

Until next time,
Deepanshu

Backend engineer writing about production bugs, distributed systems, and engineering patterns learned the hard way.

Portfolio · Medium · LinkedIn · GitHub

Top comments (0)