Writing a Bug Report That Gets Fixed
The last page, debugging a failing test, ended with a package: a minimal reproduction, actual-vs-expected values, and one sharp question, handed cleanly to whoever owns the answer. A bug report is that same package, aimed at a different reader — not “which behavior is correct?” but “here is a defect; here is exactly how to see it; go fix it.”
A report is not a diary entry and it is not a complaint. It is a work order. Its entire job is to move a defect from you noticed it to someone fixed it with the fewest round-trips in between. And the property that decides whether that happens is not eloquence or urgency. It is whether the person reading it can make the bug happen on their own machine, on the first try. Everything else on this page serves that one goal.
The one property that matters most: reproducibility
Section titled “The one property that matters most: reproducibility”Most reports die in the same place: a developer opens the ticket, cannot make the bug happen, marks it “cannot reproduce,” and closes it. The defect is still there. Nothing was fixed. The only thing that happened is a round-trip that cost both of you a day.
So state the principle plainly and let everything else follow from it:
A report a developer can reproduce in one read is a report that gets fixed. A report they cannot reproduce is a conversation, at best.
Reproducibility is the highest-leverage property because it is a gate. A developer cannot fix what they cannot see fail, cannot confirm a fix for a bug they cannot trigger, and cannot regression-test it later. Every other section here — the numbered steps, the exact input, the build number, the attached log — exists to raise the probability that the reader reproduces the bug on the first attempt.
what the reporter has what the report must transfer ───────────────────── ───────────────────────────── the exact steps they took ─────► numbered, minimal, from a known start the exact data they used ─────► the literal input, not "some value" the environment it broke in ─────► OS / browser / build / config what they saw ─────► actual result, verbatim what they expected ─────► expected result, and why
└── together: the reader re-creates the failure, once ──┘If you remember one sentence from this page: you are not describing a bug, you are transferring the ability to reproduce it.
The anatomy of an actionable report
Section titled “The anatomy of an actionable report”A good report has a fixed shape. The shape is not bureaucracy — each field removes one reason the bug could stall. Here is the anatomy, in the order a reader consumes it.
1. A specific title
Section titled “1. A specific title”The title is read a hundred times — in lists, in searches, in standups — and the full report maybe twice. It must carry the defect on its own. A specific title names what breaks, where, and ideally the trigger.
VAGUE (dies in triage) SPECIFIC (routes itself) ────────────────────── ──────────────────────── "Login broken" "Login fails with 500 when email contains a '+' (staging, build 4471)" "Value wrong" "SET truncates value at first space: 'ada lovelace' stored as 'ada'" "App crashes sometimes" "Checkout crashes on iOS 17 when cart has >50 items (repro 3/3)"2. Environment / build context
Section titled “2. Environment / build context”Where did it break? A defect is often specific to a browser, an OS, a locale, a device, or — most often overlooked — an exact build number. “It fails” is unfixable if the developer is on a build where it was already fixed, or a config where it never happened. Pin the coordinates:
Environment ─────────── Build: staging, commit abc123 (build 4471) Browser: Chrome 126.0 / also repro on Firefox 127 OS: macOS 14.5 Account: test-user-free-tier (no admin) Locale/TZ: en-CA, America/Toronto Data: seeded from fixtures/orders-small.jsonThe rule of thumb: include anything that, if it were different, might make the bug disappear. That is precisely the set of things a “cannot reproduce” reply will later blame. Which environment to run in — and why staging is not production — is the subject of test environments & configuration; name the one you were in.
3. Minimal, numbered repro steps
Section titled “3. Minimal, numbered repro steps”This is the heart of the report. Numbered, so the reader can tell you “it broke at step 4.” Minimal, so there is nothing between the start and the failure that isn’t necessary — every extra step is a place the reader can diverge and fail to reproduce. Start from a known state (“logged out,” “empty cart,” “fresh WAL”), not from wherever you happened to be.
1. Start the server against an empty store (fresh WAL). 2. Connect a client: nc 127.0.0.1 <port> 3. Send exactly: SET name ada lovelace 4. Send exactly: GET nameNotice these steps are the minimized case from the previous page’s debugging — not a 30-step tour of the app. If you already shrank the failure while debugging, the shrunk case is your repro. That is the payoff of minimizing.
4. Expected result and actual result — kept separate
Section titled “4. Expected result and actual result — kept separate”Two fields, never merged, because they answer two different questions: what should have happened and what did. The gap between them is the bug, and separating them lets the reader see it instantly.
Expected: GET name → VALUE ada lovelace Actual: GET name → VALUE ada (truncated at first space)State the actual verbatim — the literal output, status code, or error string, copied, not paraphrased. State the expected with a reason if it isn’t obvious (“per the SET spec, the value is everything after the key”). “It’s broken” is not an actual result; VALUE ada is.
5. Evidence that shortens diagnosis
Section titled “5. Evidence that shortens diagnosis”Attach what turns a developer’s investigation from reconstructing your session into reading it. The best evidence lets them skip straight to the failing line:
- Logs around the failure — the server lines emitted between steps 3 and 4, not the whole file.
- A stack trace / backtrace, complete and unedited — the single most diagnosis-shortening artifact for a crash.
- A screenshot or screen recording for anything visual or timing-related; a 10-second recording often replaces a paragraph.
- The exact data / input used — the literal
SET name ada lovelace, the request body, the fixture file, the seed. “Some name with a space” costs a round-trip; the exact bytes cost nothing.
Put together, the anatomy looks like this — a template you can paste into any tracker:
TITLE: SET truncates value at first space ENVIRONMENT: build 4471, staging, fresh WAL STEPS: 1. start server, empty store 2. SET name ada lovelace 3. GET name EXPECTED: VALUE ada lovelace ACTUAL: VALUE ada EVIDENCE: server.log lines 88–91 (attached); parser splits on ' ' SEVERITY: High (silent data loss) PRIORITY: (owner sets)Severity vs priority: two axes, two owners
Section titled “Severity vs priority: two axes, two owners”These two words are used interchangeably in hallways and it causes real confusion, so pin them apart. They are different axes, set by different people, for different reasons.
- Severity = how bad is the impact if this bug is hit? It is a property of the defect, largely objective, and it is yours to set as the reporter — you saw the impact. Data loss and crashes are high severity; a misaligned label is low.
- Priority = how soon should we fix it, relative to everything else? It is a scheduling decision, and it belongs to whoever owns the backlog — a lead, a PM, a triage owner. It weighs severity against frequency, business cost, release timing, and effort.
They are not the same axis, and the interesting cases are where they diverge:
HIGH severity LOW severity ┌───────────────────────┬───────────────────────┐ HIGH │ Checkout crashes for │ Company logo wrong on │ priority │ all users — fix now │ the homepage — fix now │ │ │ (CEO is watching) │ ├───────────────────────┼───────────────────────┤ LOW │ Data corruption, but │ Typo on an internal │ priority │ only on a device we │ admin page seen by │ │ dropped support for │ three staff — someday │ └───────────────────────┴───────────────────────┘A crash on dead hardware is high severity, low priority. A wrong logo is low severity, high priority. Your job is to report severity honestly and give the evidence — frequency, blast radius — that lets the owner set priority well. Do not inflate severity to force priority; that is a fast way to have all your reports discounted.
Write for the reader
Section titled “Write for the reader”A report is read by a tired human who did not see what you saw. Three habits make you the reporter whose tickets get picked up first.
No blame, no tone. “The dev clearly didn’t test this” adds zero information and makes the reader defensive. Report the defect, not a verdict on the person. The bug is a fact about the code; keep it that way. This is the same collaboration posture the whole part is built on and the next page, working with developers, makes explicit.
No vague “it’s broken.” “Broken,” “doesn’t work,” “acts weird” transfer a feeling, not a failure. Replace every one with the actual observed output. If you cannot state the actual result concretely, you have not finished investigating — go back to the debugging method before you file.
One defect per report. The temptation is to bundle three annoyances into one ticket to “save time.” It costs time: the three bugs have different severities, different owners, different fixes, and different lifetimes. When one is fixed, the ticket can’t be closed; when priorities differ, it can’t be scheduled. Split them. One report = one defect = one clean close.
BUNDLED (unresolvable) SPLIT (each closes cleanly) ────────────────────── ─────────────────────────── "Login is a mess: 500 on '+', #101 Login 500 on '+' in email logo blurry, and it's slow" #102 Logo blurry on login (retina) #103 Login TTFB > 3s on cold cachePre-empt the three classic rejections
Section titled “Pre-empt the three classic rejections”Almost every rejected report is closed with one of three phrases. The professional move is to write the report so that each phrase is already answered before the reader could reach for it.
REJECTION WHAT IT REALLY MEANS PRE-EMPT IT WITH ───────── ──────────────────── ──────────────── "cannot reproduce" your steps didn't work minimal numbered steps on my machine from a known start + exact input + build/env
"works as designed" you and the spec disagree the expected result WITH about 'expected' its source (spec/ticket), not just your assumption
"need more info" a field you left blank fill the anatomy: env, blocked me evidence, exact actual, repro count (e.g. 3/3)Read your draft once as the developer who wants to close it fastest. Wherever they could reach for one of these three phrases, you have a hole — fill it before you submit. A report with no holes doesn’t bounce.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because the developer who fixes a bug is almost never the person who found it, and a report is the only bridge across that gap — it transfers the ability to reproduce a failure from one machine and mind to another.
- What problem does it solve? It kills the round-trip. A report with steps, exact input, environment, and evidence lets a fix happen in one pass instead of a multi-day volley of “cannot reproduce / need more info.”
- What are the trade-offs? Writing a good report costs the reporter real minutes up front — titling precisely, minimizing steps, attaching logs. That cost is paid once and saves the far larger, repeated cost of clarification round-trips and reopened tickets.
- When should I avoid it? Never skip reproducibility or the expected/actual split. You can go lighter on ceremony — full env tables, formal severity fields — for a trivial, obvious defect you are fixing yourself in the same sitting, where there is no one on the other side to communicate to.
- What breaks if I remove it? Defects rot in the tracker marked “cannot reproduce,” severity and priority blur into a single inflated number nobody trusts, bundled tickets can never be closed, and the signal your testing produced never becomes a fix. The whole value of finding the bug leaks away at the last step.
Check your understanding
Section titled “Check your understanding”- Why is reproducibility described as the single highest-leverage property of a bug report, rather than one field among many?
- A teammate files: “Search is broken, returns garbage.” Rewrite it as a specific title and name the three anatomy fields most likely missing.
- Distinguish severity from priority. Give one bug that is high severity but low priority, and one that is low severity but high priority, and say who sets each axis.
- Name the three classic rejection phrases and, for each, the specific part of the report anatomy that pre-empts it.
- Why is “one defect per report” worth insisting on even when three bugs were found in the same five-minute session?
Show answers
- Because it is a gate, not a nicety: a developer can’t fix what they can’t make fail, can’t confirm a fix for a bug they can’t trigger, and can’t regression-test it. Every other field (steps, input, env, evidence) exists only to raise the odds the reader reproduces the failure on the first read — so reproducibility is the property they all serve.
- Example: “Search returns results for the wrong tenant when the query contains a space (staging, build 4471).” The missing fields are almost certainly repro steps (numbered, from a known start), the exact input/data used, and actual vs expected stated verbatim (plus environment/build).
- Severity = impact if the bug is hit (a property of the defect; set by the reporter). Priority = how soon to fix relative to everything else (a scheduling call; set by the backlog owner / lead / PM). High-severity-low-priority: data corruption only on hardware you’ve dropped support for. Low-severity-high-priority: a wrong company logo on the homepage the CEO wants fixed now.
- “Cannot reproduce” → minimal numbered steps from a known start, exact input, and build/env. “Works as designed” → the expected result with its source (spec or ticket), not just your assumption. “Need more info” → fill the whole anatomy: environment, evidence, verbatim actual result, and a repro count like 3/3.
- Because the three bugs have different severities, owners, fixes, and lifetimes. Bundled, the ticket can’t be closed when only one is fixed and can’t be scheduled when priorities differ. Split, each report is one defect that closes cleanly — the small filing cost buys a tracker that actually tracks.