Bug Report Examples: Good, Bad and Fixable
By Sebastiaan Jansen · · 17 min read
A bug report example is only useful if a second person can reproduce the failure without asking you anything. So it names the steps, the expected result, the actual result, and the environment. "Checkout is broken" tells engineering you are unhappy, but it doesn't tell them what to do.
This post walks through one weak report, the same incident rewritten, and the fields that close the gap. You can copy the good version into Jira, GitHub Issues, Bugzilla, or any other tracker.
Bug report example: good, bad, and fixable
A good report answers five questions in order: what broke, who it affects, how to see it, what should have happened, and how bad it is. A bad report answers "what broke" and leaves the rest for a meeting.
Treat the bug report example as a form you fill in. The person fixing the bug often did not see it, and may open the ticket days later with no memory of the Slack thread. The ticket has to do four things on its own: separate a bug from a change request or a duplicate, reproduce the failure on a known build, show the impact on users, and let someone confirm the fix with the same steps. If any of those needs the author to be online, the report isn't finished.
A report also isn't a theory about the root cause. "Pay now returns HTTP 502 for saved cards on Chrome 131 after 3D Secure" is a report. "The payment vendor was a bad choice" is a guess. "There is no export button" describes a missing capability, while "Export downloads an empty CSV for last month" is a bug, because the product claims to do the thing and then does it wrong.
One bad report, line by line
This fictional report is typical of what comes in from a client call or a Slack message:
Checkout is broken again. Customers are complaining. Can someone look today? It worked last week. Screenshot attached.
The screenshot is a cropped toast that says "Something went wrong." There is no order id, browser, account, or time.
| Gap | What engineering has to do instead |
|---|---|
| No steps | Guess the button, the page, and the user |
| No expected result | Argue about whether the toast is the designed error |
| No environment | Retest on the wrong browser, build, or region |
| No scope | Treat one failed order as "all customers" |
| No build | Bisect "last week" across every deploy |
| "Again" with no link | Hunt for the previous ticket by memory |
"ASAP" tells you how urgent it feels and nothing about the defect. What helps is writing the same incident as fields.
Split that message into facts and leftovers. "Checkout is broken" names an area but hides the button. "Customers are complaining" has no account and no count, so it cannot set priority. "It worked last week" is a regression hint until someone names a build, and the cropped toast could belong to any page. What you can keep is the area, the toast, and the claim that this is a repeat. The sample below adds only what was observed and marks Firefox and production as unknown, because a guess written down as a fact tends to come back as "cannot reproduce."
The fields that make a report fixable
Eight fields cover almost every web or app bug. A fuller list sits in Bug Report Template: The 8 Fields Developers Actually Need.
- Title. Area, action, failure. "Checkout: Pay now returns 502 for saved cards after 3D Secure."
- Environment. Version or git SHA, browser or device, OS, account type, region, and the URL. For a mobile app, add the build number.
- Steps. Numbered actions a stranger can follow, starting from a known state.
- Expected. What the product should do.
- Actual. What happened, with the exact error text, HTTP status, or error id.
- Frequency. Always, intermittent (say 2 of 5 tries), or once, with a rough time if it was once.
- Impact. Who is blocked, and whether there is a workaround. This feeds into priority, but it doesn't replace steps.
- Evidence. A full-window screenshot or short recording, plus a log line, Sentry event, or request id. Redact secrets.
Severity is how broken the behavior is. Priority is when the team will work on it. A typo on the homepage can be low severity and high priority, while a crash in an admin tool used once a quarter can be high severity and low priority. Write down both, or every "urgent" label ends up meaning both at once.
Write steps as actions you can repeat. Nobody can retest "I tried to pay and it failed after the bank thing." They can retest "Sign in as this user, choose the saved Visa, pass 3D Secure, click Pay now, see HTTP 502." Each step is one action. "Go to checkout and pay" hides the item, the payment method, and the button. If a saved address is required, put that in preconditions so nobody finds out on step 4. "Open the menu, then Account, then Payment methods" can be repeated; "Find your cards in settings" cannot.
Start from a named state. "Be logged in" is weaker than "Sign in as qa+savedcard@example.com," or "any customer account with one saved card." If you do not know the starting state, write "(unknown)" rather than guessing, since a guessed precondition is a common source of false "cannot reproduce" results.
Expected and actual have to be checkable on the same screen or the same response. "It should work" is not an expected result. "The confirmation page loads and the response includes an order id" is. Quote the product word for word: if the toast says "Something went wrong" and the body says issuer_timeout, record both. A paraphrase can send the fix to the wrong string.
Frequency needs a denominator. "Sometimes" can mean once a month or four tries out of five, while "3 of 3 at 14:10 UTC" says you repeated it. A single sighting is still valid if you call it once and give the time. Calling it constant sends someone after a bug that may already be gone.
Impact means who cannot finish which job, and what still works. Name the failing path and the working one; that is how a staging-only defect stays off the production-outage list. If you do not know about production, write that down. Saying nothing reads as "not affected," when you have no evidence either way.
Attach the whole window so the URL and the error show up together, and crop only to hide secrets. When the bug is a race, a short recording beats stills. Paste a request id or a Sentry link rather than a card number, a session cookie, or a database row. Even then, a stack trace doesn't know which button was pressed. Sentry errors filed as bug tickets can open the ticket straight from the event, but the report stays incomplete until a person adds how to see the failure.
Redact before you attach, not after someone has already copied the ticket into a channel. Card numbers, full session cookies, passwords, reset links, and government ids do not help reproduction. The last four digits of a test card, a request id, and a role name do. Cover a customer's name unless you need it to sign in, and restrict the ticket if that named account is the only one that fails.
On a phone, add the build number from the app's settings, the OS version, and the device model. A layout bug on a small phone is a different ticket from a crash on a tablet, and "latest iOS" will mean something else next month. If the build came from TestFlight or a store, say which, because those are not the same binary.
A sample bug report worth sending
This sample bug report is the same checkout incident, written so triage can accept it. The shop, the error, and the ids are made up, so copy the shape and ignore the story.
Title: Checkout: Pay now returns 502 for saved cards after 3D Secure
Environment: Web app 2026.10.04 (SHA a1b2c3d on main), Chrome 131, macOS 15, staging, customer account, region EU.
Preconditions: User has one saved Visa. Cart total is under the manual-review threshold.
Steps:
- Sign in as
qa+savedcard@example.com. - Add SKU
MUG-BLUEand open/checkout. - Select the card ending 4242 and pass the 3D Secure test challenge.
- Click Pay now.
Expected: Confirmation page, and POST /api/checkout returns 200 with an order id.
Actual: Toast "Something went wrong." POST /api/checkout returns 502 with {"error":"issuer_timeout"}. No order is created. Request id req_9f3a. Reproduced 3 of 3 times at 14:10 UTC.
Impact: Staging only so far. Saved-card checkout is blocked. Guest checkout with a new card still works. Support has no matching production reports yet.
Severity: High for this payment path. Priority: Medium until production confirms it. Workaround: Pay with a new card.
Evidence: Full-window screenshot of the toast. Sentry event CHECKOUT-502. Request id above.
Notes: Guest checkout on the same build succeeds. I did not try Firefox. A negative result is more useful than a claim that every browser fails.
A client won't know the SHA. Ask them for the URL, the time, the account, the clicks, and a full-window screenshot, and let engineering add the build during triage. This sample bug report example shows that handoff: the client's raw observation goes in, and fields come out.
If you rewrite a live ticket, leave the reporter's original words in the description or a comment. You retest against the fields, but you need the original wording when the reporter says you changed their meaning. Keep "again" out of the title. If a previous ticket exists, link it.
How to fix a report that arrived broken
Most reports arrive incomplete. If you bounce them with "please use the template" and offer no help, people stop reporting. Rewrite the report in a comment instead, and ask only about the gaps you can't fill yourself.
- Set the title to area, action, and failure. Keep the reporter's wording in the description if it adds context.
- Fill in the environment from the release, user agent, tenant, and URL you already have.
- Turn the paragraph into numbered steps. Mark any step you inferred with "(assumed)".
- Write expected and actual as two lines. If expected is unclear, ask one question.
- Set severity from the behavior and priority from the impact, with one line explaining why.
- Link the earlier ticket, or write "no prior ticket found."
Rewrote the title and steps from the screenshot and request id
req_9f3a. I am assuming the saved Visa, because guest pay still works. Correct me if the card was different. Severity is high and priority is medium until production confirms it. Workaround: pay with a new card.
Ask one question at a time. Someone can answer "Which card did you use?" from a phone. "Please fill in the template" often goes nowhere, because the reporter has already said everything they think they know. A ticket with two unknown fields and a clear question keeps moving; one sent back with six blanks sits and waits.
Mark assumptions in the step where they sit. "(assumed) Select the saved Visa" tells the reporter which sentence to correct. A clean list that hides a guess looks authoritative and then fails on the first retest. When the reporter replies "it was a new card," edit the step itself. Don't leave the correction in a comment under a description that still says Visa, because the next reader will follow the description.
If the same thin report shows up every week, make steps, expected, and actual required on the form. Required boxes won't stop someone from typing "then it breaks," though, so reject entries that say nothing and put one filled-in incident in the form's help text. When a template has no example, people copy the bare headings. When it has one filled incident, they copy the fields.
Mistakes that survive a template
A form can contain every heading and still be a bad report.
The title names the area and hides the failure. "Checkout bug" does not say which button or which result. Write the action and the failure: Pay now, then 502.
The steps include the bug as if it were an action. "Step 4: The payment fails" belongs in Actual. Steps stop at the last thing the tester does, so "Click Pay now" is the step and the 502 is the result.
Expected copies the complaint. "Pay now should not return 502" just restates the title. Say what a correct run leaves on screen: a confirmation page and an order id. That sentence is also the check you run after the fix.
The environment says "latest" or only "production." Two deploys can share a day, so pin a SHA, a build number, or a release name. If the screen does not show it, say where you looked and leave the build for triage.
Actual describes a feeling. "The page feels broken" can sit in Impact if it changes what users do. The result itself is the control, the message, or the wrong number.
Scope gets widened on the way in. One failed card becomes "payments are down." Write the cases you ran: one brand, one tenant, one browser. A wider claim without a wider check is how someone tries a random account and fails to reproduce the bug.
Attachments arrive with no pointer. Name the file and the frame: "0:12 shows the toast while the URL is still /checkout." Leave the HAR in the attachment, but keep the request id in the description so the ticket makes sense before anyone opens the file.
Triage: from report to a ticket someone owns
A report isn't scheduled work yet. Keep the acceptance rules short, and apply the same ones to every reporter:
- A ticket needs steps, expected, actual, and an environment, or a named owner who will add them within one working day.
- Duplicates get linked and closed, and any new evidence is copied onto the open ticket.
- Regressions name the last good build when someone knows it. "Worked last week" is a useful hint, but it can't go in a field.
- The triage owner sets severity. Reporters can propose it.
- "Cannot reproduce" records the build and the steps that were tried, then goes back to the reporter once before it is closed.
Treat "cannot reproduce" as a result and write it up like one: the build, the account, and the steps you tried, including any assumed step you could not match. "Tried the four steps on 2026.10.04, guest checkout only, no saved card on this account" is a result. "Doesn't happen for me" reads as a dismissal. Send it back once. If the reply still can't be retested, close it as incomplete and leave the attempted steps so the next report can be compared with them.
When you close a duplicate, copy new evidence onto the open ticket first (a better request id, a second browser, a worse screenshot) and say what you copied. Otherwise the Firefox result lives only on the closed ticket. Don't merge different failures that share a button: "Pay now returns 502" and "Pay now stays disabled" are separate bugs.
Each tool enforces these gates its own way, but they all ask for the same facts. Jira Bug Tracking: Setup, Workflow and Common Mistakes uses workflow conditions. GitHub Issues as a Bug Tracker: Strengths and Gaps uses issue forms. Bug Tracking Software: How to Choose One for a Small Dev Team puts the same list in buying terms: a place for the report, a queue, an owner, and a state that means the work can start.
Many reports start life in Slack, and a Slack message has no required fields, no owner, and no "done." Decide when a message becomes a ticket, and copy the steps across when it does. Slack Ticketing System: Turning Requests Into Tracked Work covers that handoff.
Falrow is one place that path can end up. Slack messages and client calls become tickets there, and triage follows the rules on ticket types and stages. Jira workflows and GitHub issue forms can enforce the same gate. None of these tools will write the steps for you.
Back to the checkout case: "Saved-card checkout fails on staging, guest pay works, no production reports" is high severity and medium priority. Raise or lower the priority when production evidence arrives. Link duplicates and copy any new steps onto the original. If it fails again on a build that was marked fixed, file a regression that names that build.
A regression report should name two builds when you can: where the steps fail now, and where those same steps last passed. "Last week" can start the search, but the field needs a version or an honest blank, plus a link to the ticket that closed the earlier fix. Keep the original steps. "It broke again" lets the next owner invent a different path and miss the card type.
When production evidence arrives, add it under Impact and update priority in place: "2026-10-06: two production reports, same issuer_timeout, priority raised." If the old priority stays, anyone sorting the queue misses the new scope.
One more case: a bug that looks like a feature gap
The checkout ticket is a loud failure with an error code. Many reports are quieter, and the bad version sounds like a feature request. This second fictional case uses made-up names. It uses the same fields, though this time there is no stack trace.
A support agent writes: "Search is useless for Northwind. They can't find orders. Can we make search better?"
You could read that as a request for a new search. The retest says otherwise.
Title: Orders search: SKU query returns no rows when the status filter is still "Unpaid"
Environment: Web app 2026.10.04, Firefox 132, Windows 11, production tenant Northwind, admin role.
Preconditions: The tenant has paid orders whose SKU contains MUG. The orders list was last left on status "Unpaid" by this admin.
Steps:
- Sign in as an admin on the Northwind tenant.
- Open
/orders. Confirm the status control still says Unpaid. - Search for
MUG.
Expected: Rows for paid orders that contain that SKU, or a visible statement that the Unpaid filter is still applied so the empty list is explained.
Actual: The table shows an empty state, "No orders," and the Unpaid filter stays selected with no message that it is hiding matches. Clearing the filter and searching again shows the paid rows.
Frequency: 3 of 3 for this admin. A second admin whose status filter was "Any" saw the rows on the first search.
Impact: One tenant so far. Admins who do not notice the leftover filter think those orders are gone. Workaround: set status to Any, then search.
Severity: Medium. The data is still there. Priority: Medium if more than one tenant works this way, low if Northwind is the only report. Evidence: Full-window screenshot showing the empty table, the Unpaid control, and the query string.
This is a bug because the empty list hides an active filter. "Make search better" would have sent a team off to redesign ranking, while the steps point to a stuck control. Triage can still choose a design change, but now it decides about a screen someone can open.
If the steps had instead shown that search has no SKU field, and the product never offered one, the ticket would be a request. Write the expected behavior anyway. The type can change later; the steps should not.
Habits that keep the next report short
Reproduce the bug before you assign it. If triage can't follow the steps, send the ticket back and name the missing field.
For intermittent bugs, give a count and say what differed between tries. "Not Firefox. Not guest checkout." often helps more than another screenshot. Keep one reproduction in the description and treat comments as history. GitHub's issue docs draw the same line between the body and the discussion (GitHub Issues documentation).
Write down what you held constant. On the checkout bug the cart, the card, the browser, and the build stayed the same, and the failure showed up 3 of 3 times. A failure that appears only when a second tab refreshes the same cart is a different case. Report a correlation as a correlation: "failed on the two tries where 3D Secure took longer than a minute; I did not time the fast path." Keep your theory about the patch out of Actual, or the next reader will treat it as something you saw.
After a fix, run the original steps on the build that claims to contain it, then run the path that was supposed to keep working. For this report that means saved-card pay, then guest pay. If the 502 goes away but guest pay starts failing, the fix isn't done. Paste the result next to the old build: "Verified on 2026.10.06, steps 1 to 4, confirmation page, guest pay still returns an order id." A bare "please confirm" with no build is how the same bug gets closed twice.
Don't ask reporters for a root cause. A support agent should be able to file the sample without knowing a git SHA. Call notes become a report once someone pulls out the steps, expected, and actual. Falrow can turn a reviewed meeting note into a ticket, and a plain checklist works too if the team uses it. The report is done when a teammate who missed the conversation can reproduce the bug.
Keep the description as the current way to reproduce the bug. New attempts go in comments with a date and a build. If a step changes, edit the step and add one line at the top: "Steps updated on 2026-10-06: card brand is Mastercard, not Visa." Otherwise someone verifies the fix against comment 14 and a stale description, which means they verified it against the wrong card.
FAQ
What should a bug report example include?
A specific title, the environment, numbered steps, the expected result, the actual result, how often it happens, the impact on users, and one piece of evidence such as a screenshot or request id. Set severity and priority separately, because a severe bug isn't always the next thing the team should work on. Logs and related tickets help, but they don't replace steps.
How long should a bug report be?
Long enough that a stranger can reproduce the bug, and no longer. Written as fields instead of a story, most web bugs fit in half a page. Put a stack trace or a HAR file in an attachment, and keep the description short so the next person sees the steps without scrolling past a diary.
What is the difference between a bug report and a feature request?
A bug report says the product failed at something it is supposed to do. A feature request asks for behavior the product doesn't claim to have yet. "Export downloads an empty file" is a bug; "We need an export button" is a request. Labeling requests as bugs buries real regressions. If you're unsure, write down the expected behavior and let triage pick the type.
Do I need a screenshot in every bug report?
No. A screenshot helps when the failure is visual or the error text is easy to copy wrong. It doesn't replace steps, and a cropped toast with no URL often adds nothing. For API and data bugs, a request id or a query is better evidence than an image. When the bug depends on timing, use a short recording.