Building a Minimal Reproduction Before You Ask Anyone

Learn how to build a minimal reproduction by isolating code, data, dependencies, and configuration so you can find bugs faster and report them clearly.

Developer workflow reducing a complex codebase step by step until a small reproducible example still triggers the same error.

Most advice about minimal reproductions frames them as politeness: be considerate, do not dump 4,000 lines into an issue, respect the maintainer’s time. That framing is true and it is also why the technique gets skipped, because politeness loses to a deadline every time.

Here is the framing that survives a deadline. The reduction is a debugging technique. Finding the answer yourself is the normal outcome, and the postable example is what is left over when you do not.

If you have ever cut a file down to twenty lines and watched the bug vanish at line nineteen, you have already used it. This page is about doing that deliberately instead of by accident.

Start with the thing you can run twice

Before you cut anything, you need a trigger you trust. A reduction is a sequence of experiments, and an experiment needs a result you can compare against the previous one.

So the first question is not “what can I delete”, it is “what exact sequence makes this happen, every time?”

Write it down as steps, not as prose:

1. Load /checkout with an empty cart
2. Add product 41
3. Reload
4. Total shows 0.00 instead of 19.99

If step 4 only happens sometimes, stop. An intermittent trigger will lie to you throughout the reduction: you will delete an innocent line, fail to reproduce on that run, and conclude you found the cause. Chase the intermittency first. Usually it is state left behind between runs, a cache, a session, a stored row, or a race in whatever loads first.

Cut in halves, not in nibbles

The instinct is to remove one suspicious thing at a time. That is slow, and worse, it biases the search toward whatever you already suspect.

Bisect instead. Delete half of what remains, run the trigger, and keep whichever half still fails.

600 lines  -> delete second half -> still fails -> keep first 300
300 lines  -> delete second half -> passes     -> restore, delete first half instead
150 lines  -> still fails -> keep
 ...

The number of steps grows with the logarithm of the file size, not with the file size, which is why this feels almost unfairly fast the first time you do it properly. Six hundred lines is around ten runs.

Two rules make it reliable:

  • Comment out, do not delete. You will need to restore a half you were wrong about, and reaching for undo ten levels deep is how you lose the reduction.
  • One cut per run. Two changes and a passing run tells you nothing about which change mattered.

Work on a copy, or on a scratch branch. Never bisect in the file the rest of the team is editing.

Cut the environment too, not just the code

Code is only one axis. The reduction is not finished until you have also tried removing:

  • Data. One row instead of the whole table. The specific row that fails is often the answer on its own, because it holds a null, an unexpected character set, or a value at a boundary.
  • Dependencies. Disable third-party packages one group at a time. If the failure survives with every optional dependency gone, you have eliminated an entire class of suspect.
  • Configuration. Default config, default theme, default locale. A bug that only reproduces under one setting is a bug about that setting.
  • The network. Replace a live API call with a hard-coded response captured from a real one. If the failure follows the captured response, the problem is your parsing. If it disappears, the problem is the call.

Each axis you can rule out narrows the report enormously, and each one is a plausible place for the bug to actually live.

Reduction is what stops you fixing the wrong line

There is a specific failure this technique prevents, and it is common enough to be worth naming: fixing the place where the symptom appeared rather than the place where the wrong value was produced. The stack frame you were handed is where execution stopped, not necessarily where the mistake was made, which is a distinction worth understanding in its own right, and one we take apart in why the error line is often not where the bug is.

A reduction resolves that ambiguity mechanically. When the failing example is thirty lines long, there is nowhere for the real cause to hide. You are no longer reasoning about which of two files is guilty, you are looking at both of them at once.

The same protection applies to failures that never raise an error at all. Silent wrong answers are the hardest category to bisect toward, because “still fails” means “still returns the wrong number” rather than “still throws”, and you have to assert on the value by hand at every step. That extra discipline is exactly what makes debugging a wrong result with no error message tractable.

When to stop cutting

Stop when either of these is true:

  1. Removing anything else makes the failure disappear. What remains is the reproduction, and it is also, very often, the explanation.
  2. You have understood the cause. You do not need a publishable example to fix your own bug. Close the file and write the fix.

Do not keep cutting for elegance. A forty-line example that reproduces is worth more than a fifteen-line one you spent another hour on.

What the finished report contains

If you are posting it, four things, in this order:

  • The reduced code, as text, in a fenced block. Not a screenshot. Nobody can run a screenshot.
  • The exact trigger, the numbered steps from earlier.
  • Expected and actual, both stated. “Expected 19.99, got 0.00” is a complete sentence. “It does not work” is not.
  • Versions, of the language, the framework and the relevant package. Whether the reader can reproduce your failure often depends entirely on this line, and it is the single most frequently omitted one.

Then say what you already ruled out. That is what your bisect produced, and it is the part that saves the responder the most time.

The other reason to build one

There is a second use for this skill that has nothing to do with issue trackers. When you are handed a large change you did not write, and something in it is wrong, the reduction is how you find out which part. Isolating one behavior from a wide diff is the same operation as isolating one behavior from a wide file, and it is a core move when reviewing a pull request that touches forty files.

The technique transfers because the underlying question is the same one every time: what is the smallest thing that still misbehaves? Answer that and you have either fixed the bug or written the report. Both are wins, and you cannot tell in advance which one you are going to get.

Written by

Shah Alom

Leave a Reply

Your email address will not be published. Required fields are marked *