Bug Analyst logo
4 min read

Reproducing hard-to-reproduce bugs: a field guide

Buganalyst

Some bugs reproduce on the first try, get fixed by lunch, and are forgotten. Others become legends: reported repeatedly over months, never once reproduced by a developer, eventually closed with the tracker's most defeated status — cannot reproduce. The second kind is rarely random. Bugs that resist reproduction almost always depend on a condition the reporter did not know was relevant and the developer did not think to vary. Cornering them is a systematic activity, not luck.

Start by believing the report

The first discipline is psychological. When a bug will not reproduce, the tempting conclusion is that the user is mistaken. Occasionally true — but treating it as the default hypothesis ends investigations before they start. Users almost never invent error states from nothing. A better default: the bug is real and happens under conditions you have not matched yet. Your job is not to confirm the bug exists; it is to find the difference between the reporter's world and yours.

Enumerate the dimensions of difference

Every "works on my machine" gap lives along a small set of dimensions. Walk them deliberately rather than intuitively.

  • Environment: browser and version, OS, device class, screen size. Layout and input bugs are notoriously viewport-dependent — reproducing at the reporter's exact dimensions is often the entire trick.
  • State: which account, which role, which data. A user with 4,000 records exercises pagination, rendering, and timeouts in ways your three-item test account never will. Empty states and enormous states are both bug nurseries.
  • Timing: slow networks, quick double-clicks, actions taken while a background request is still in flight. Throttle your connection in dev tools and a whole family of race conditions surfaces on demand.
  • History: cache contents, cookies, localStorage, an old session, a stale service worker. A hard refresh or incognito window differing from a normal window is itself a diagnostic result — it points directly at cached state.
  • Sequence: the exact order of actions, including the ones the reporter did before the "steps" nominally began. The report says "clicked export, got an error"; the missing ingredient is that they had opened the filter panel first.

Most stubborn bugs fall to a methodical pass over those five dimensions. The failures come from varying one dimension while unknowingly holding the wrong value on another.

Get the context you should have gotten automatically

Half the dimensions above — URL, viewport, user agent, timing — are exactly the fields a well-instrumented bug report carries with it. If your reports arrive through an embedded widget, you already have the environment half of the puzzle attached to every report, and reproduction starts from second base. If your reports arrive as prose in email, the first investigative step is an interview, which costs a day per question. This is the practical case for capturing context at submission time: it converts archaeology into lookup.

When you do have to interview, ask for concrete artifacts rather than descriptions: the exact URL from the address bar, a screenshot including the full window, a screen recording of one more attempt. People are poor narrators of their own actions and excellent screen-recorders.

Intermittent bugs: change the question

For true heisenbugs — the ones that fail one time in fifty — stop asking "how do I make it happen?" and ask "what do all the occurrences have in common?" Gather every report of the phenomenon and lay the metadata side by side: timestamps, browsers, pages, account types. Patterns emerge with startling reliability. All the failures cluster within minutes of a deploy. All of them are Safari. All of them hit accounts in one timezone offset. The commonality is not the fix, but it collapses the search space from everywhere to somewhere.

Timestamps deserve special mention: correlating report times against deploy logs and error monitoring is the single highest-yield move for intermittent issues, and it requires no reproduction at all.

Make it reproducible on purpose

Sometimes reproduction requires making the rare condition common. Add a temporary artificial delay to the suspect request and the once-a-month race condition happens every time. Fill a staging account with production-scale data and the pagination bug appears immediately. Force the error path — make the API return the failure response deliberately — and you can watch how the UI mishandles it at leisure. Engineering the conditions is legitimate; the bug does not know whether its preconditions arose naturally.

When you truly cannot reproduce it

Occasionally, honesty requires the cannot-reproduce resolution. Do it well: document every dimension you varied, keep the report linked and searchable rather than deleted, and add lightweight instrumentation — a log line, an error boundary — around the suspect area so the next occurrence generates evidence instead of another mystery. "Cannot reproduce yet, trap set" is a fundamentally different outcome from a shrug. The bug will be back; the difference is whether you are ready for it.