How to Write a Bug Report

A bug report has one job: let someone who was not there reproduce the problem, understand its impact and fix it without asking you a single question. Reports that do this get fixed; reports that do not get labelled "cannot reproduce" and closed. The difference is not writing skill — it is including the right facts in the right order.

This guide gives the structure of a report that gets acted on, a template for Jira, GitHub Issues or any tracker, a complete example, and a clear explanation of the two fields people confuse most: severity and priority.

What a good bug report contains

  • Title — the symptom in one specific line: "Checkout returns 500 when the cart contains a discounted item" beats "Checkout broken".
  • Environment — app version or build, browser/OS or device, API environment (staging, production), account or role used.
  • Preconditions — what must be true before the steps: a logged-in user with items in the cart, a specific feature flag.
  • Steps to reproduce — numbered, minimal, exact. Include the data you used (or a generated equivalent) and the request if it is an API bug.
  • Expected result — what should have happened, ideally with a reference to the requirement or design.
  • Actual result — what happened instead, quoted exactly: the message, the status code, the wrong value.
  • Evidence — screenshot or recording, the HTTP request and response, console or server log lines, a stack trace.
  • Frequency — always, intermittent (how often), only on first load.
  • Severity and priority — how bad it is and how soon it must be fixed (see below).

Try it: AI Bug Description Generator Try it: AI Bug Title Generator

A bug report template

Title:        Checkout returns 500 when the cart contains a discounted item
Environment:  Web app 4.12.0 · Chrome 128 / Windows 11 · staging · customer role
Severity:     Major        Priority: High        Frequency: Always

Preconditions:
  - Logged in as a verified customer
  - Cart contains one item with an active 20% discount (SKU A-100)

Steps to reproduce:
  1. Open /checkout
  2. Select "Card" as the payment method
  3. Click "Place order"

Expected result:
  Order is created (201), confirmation page shows the discounted total 199.20

Actual result:
  Page shows "Something went wrong". POST /api/v1/orders returns 500:
  {"error":{"code":"INTERNAL","requestId":"9f1c…"}}

Evidence:
  - screenshot-checkout-500.png
  - request/response captured (attached)
  - server log: TypeError: cannot read "rate" of undefined (discount.ts:41)

Notes:
  Works when the cart has no discounted item. Started after release 4.12.0.

Try it: AI Bug Description Generator Try it: HTTP Request Inspector

Severity vs priority

Severity measures impact on the system or the user: how much is broken and for whom. Priority measures urgency to the business: how soon it must be fixed relative to other work. They are set by different people and often disagree.

A typo in the company name on the login page is trivial severity but high priority. An admin export that runs once a quarter and silently drops rows is Critical severity (data loss) but low priority until its next run approaches. Recording both, separately, is what lets a team plan honestly.

  • Severity — Blocker (core flow unusable, no workaround), Critical (data loss or corruption, security exposure, failed payment, or a major function broken for most users), Major (important function broken with a workaround, or few users affected), Minor (small functional, cosmetic or edge-case defect), Trivial (typo or negligible cosmetic detail) — check from the top and take the first that fits.
  • Priority — Highest / High (fix before release or now), Medium (next sprint), Low (backlog).

Try it: AI Bug Severity Suggestion Try it: AI Bug Priority Suggestion

Before you file: isolate the bug

  • Reproduce it twice. If it happens once, note it as intermittent and record what differed.
  • Reduce the steps to the minimum that still triggers it. Remove anything that does not change the outcome.
  • Change one variable at a time — different browser, different account, different data — to find the trigger.
  • Check whether it is already reported: search the tracker for the error message and the endpoint.
  • Capture the evidence while it is on screen: the request, the response, the console, the exact message.

Try it: AI Error Message Analyzer Try it: API Response Analyzer

Mistakes that get reports bounced

  • No steps, only a description of the symptom.
  • "Does not work" as the actual result, with no message, code or screenshot.
  • Several bugs in one report — each needs its own lifecycle.
  • Opinion in the title ("this is terrible") instead of the symptom.
  • Real customer data pasted into the report. Use generated test data that reproduces the same shape.
  • Missing environment — a bug that only appears on Safari on iOS wastes a day if that is not stated.

Try it: Test Data Factory

Reporting API bugs

For an API defect the reproduction is the request. Include the method, URL, headers (with tokens redacted), body, and the full response — status, headers and body — plus the request id if the API returns one. A cURL command is the most portable form: a developer can paste it and see the failure immediately.

Try it: HTTP Request Inspector Try it: cURL Generator

Frequently asked questions

What is the difference between severity and priority?

Severity is how badly the bug affects the system or users; priority is how urgently the business wants it fixed. A cosmetic bug on the homepage can be low severity and high priority.

How many steps should a bug report have?

As few as reliably reproduce the problem — usually three to seven. Each step should be one action a stranger could perform.

Should I include a screenshot?

Yes whenever the bug is visible. For API bugs include the request and response instead; for crashes include the stack trace or log lines.

What makes a good bug title?

The symptom, the place and the condition in one line: "Login page shows a blank screen after entering a wrong OTP three times".

Can AI write bug reports?

AI can turn rough notes, an error message or a captured request into a well-structured report with a clear title and suggested severity. A tester still verifies the steps and the impact before filing.

What if I cannot reproduce the bug consistently?

File it anyway, mark it intermittent, state how often it occurs and attach every piece of evidence from the times it did occur — logs, timestamps, request ids. Patterns emerge from several reports.

References

  • ISTQB Certified Tester Foundation Level (CTFL) v4.0ISTQB · 2023The foundation syllabus of software testing: test levels and types, test design techniques, test management and tools.
  • ISTQB GlossaryISTQBThe standard definitions of testing terms — test case, regression testing, severity, priority, boundary value analysis and the rest.

Practice exercise

Rewrite a weak bug report for a failed bill payment

Your task

You are testing release 7.3.0 of a fictional bank’s web banking on the staging environment. A teammate hit a problem while testing bill payments, left the draft below in the tracker without filing it, and asked you to finish it. You have since reproduced the problem yourself: your notes, the requirement and the request your teammate copied from the browser are in the material.

Turn the draft into reports a developer can act on without asking you a single question. First decide what in the draft is a separate defect, what belongs inside the main report and what is not a defect at all. Then write the complete report for the payment failure with every field from the guide — title, environment, preconditions, steps to reproduce, expected result, actual result, evidence, frequency, severity and priority — plus notes on what you isolated. For the second defect, a title with a proposed severity and priority is enough.

Pen and paper are enough; allow about 25 minutes. The cURL Parser splits the copied command into its headers so you can decide which to keep, and the cURL Generator rebuilds a clean command. If you want a comparison afterwards, the AI Bug Description Generator from the guide’s tool list (it needs sign-in) drafts a report from the same notes — paste them without the token or the cookies, and judge its draft against your own version, not the other way round.

Material
Fictional bank, web banking. Release 7.3.0 is in testing on the staging environment.
Production and the UAT environment still run 7.2.4.

Requirement BILL-3: Pay a bill
The customer pays a saved biller any amount from 1.00 to 10,000.00 SAR, with up to two
decimal places, from one of their accounts. On success the payment appears in the
account history with a reference number, and the confirmation page shows that number.

--- Draft left in the tracker by a teammate (not filed) ---
Title: Bill payment totally broken!!! URGENT
Description: tried to pay the electricity bill, doesn't work, error every time. Also
the pay button is cut off in Arabic on mobile and the error message is useless, it
should say what is wrong. Please fix ASAP.
Request (Copy as cURL from the browser):
curl 'https://staging.bank.example/api/v2/bill-payments' \
  -H 'accept: application/json' \
  -H 'accept-language: en-US,en;q=0.9' \
  -H 'authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LWN1c3QtMDcifQ...' \
  -H 'content-type: application/json' \
  -b 'SESSION=7f3c9a1e52d84b0c; _ga=GA1.2.884120.1727' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/129.0.0.0' \
  --data-raw '{"billerId":"BLR-ELEC-01","fromAccount":"ACC-TEST-0007","amount":"250.75"}'
Response: 500
{"error":{"code":"INTERNAL","message":"Unexpected error","requestId":"b7e2-41c9"}}

--- Your notes from reproducing it (staging, build 7.3.0, retail customer, English UI) ---
Flow: Payments > Pay a bill > choose biller > choose account > Amount > "Pay bill".
Run 1: Chrome 129 / Windows 11, test-cust-07, biller Electricity, 250.00: paid,
  reference shown, balance down by 250.00.
Run 2: as run 1 with 250.75: red banner "Something went wrong. Please try again
  later." POST /api/v2/bill-payments returned 500 with the response above.
  No payment in the history, balance unchanged.
Run 3: repeat of run 2: same result.
Run 4: Firefox 131 / Windows 11, test-cust-07, Electricity, 250.75: same result.
Run 5: Chrome, test-cust-12, biller Water, 99.50: same result.
Run 6: Chrome, test-cust-07, Electricity, 1000.00: paid, reference shown.
Run 7: UAT (build 7.2.4), Chrome, uat-cust-03, Electricity, 250.75: paid,
  reference shown.
Server log viewer, requestId b7e2-41c9 (exception and first application frame):
  java.lang.NumberFormatException: For input string: "250.75"
    at bank.payments.AmountMapper.toMinorUnits(AmountMapper.java:58)
Tracker search for "bill-payments" and "NumberFormatException": no open defect.
Mobile check: Chrome device toolbar, 360 px wide. In the Arabic UI the label of the
  pay button is cut off mid-word; at 412 px it fits. The English label fits at
  360 px. Tapping the button still works.
Steps
  1. Read the draft and list every separate problem it mentions. For each one decide: its own report, part of the main report, or not a defect.
  2. Compare the runs in your notes one variable at a time: browser, account, biller, amount. Write down what triggers the failure and what makes no difference.
  3. Write the title: the symptom, the place and the condition in one line, with no opinion.
  4. Write the environment, the preconditions and the minimal numbered steps with the exact data, then the expected result with its reference to BILL-3 and the actual result quoted exactly.
  5. Clean the evidence: keep only what reproduces the request, put a placeholder in place of the bearer token, remove the cookies, and drop the headers the browser adds on its own.
  6. Set frequency, severity and priority on the guide’s scales, each with one sentence of justification; then write the title, severity and priority of the second defect.
Show the model answer
Model answer

Sorting the draft (step 1). It mixes three problems. First, the bill payment fails for some amounts: that is the main defect and gets its own report. Second, the pay button label is cut off in the Arabic UI at a narrow width: a separate defect with its own cause, fix and lifecycle, so it gets its own report too. Third, "the error message is useless": a generic message is reasonable behaviour for an unexpected server error, BILL-3 says nothing about error wording, and once the 500 is fixed these payments no longer show it. It is not a defect; if you think the wording should change, raise it with the product owner as a separate improvement suggestion. The copied request and the 500 response are not problems of their own: once cleaned (step 5), they go inside the main report as evidence. The rest goes into no report: "totally broken", "URGENT" and "ASAP" are opinion, and "error every time" is an unmeasured claim that runs 1 and 6 disprove.

Isolating the trigger (step 2). Runs 1 and 2 differ only in the amount: 250.00 succeeds and 250.75 fails, and run 3 repeats run 2 with the same result, so it is not a one-off. Changing the browser (run 4) changes nothing. Run 5 changes the account, the biller and the amount at once and still fails, so the failure needs none of those particular values. Run 6 (1000.00) succeeds like run 1. The trigger is therefore an amount with a non-zero decimal part. Run 7 succeeds on 7.2.4, which points to a regression introduced in 7.3.0; but UAT differs from staging in more than the build, so report it as a strong hint, not as a proven fact. The log line tells the developer where to look (AmountMapper.java:58), but the tester gives it as evidence, not as a diagnosis in the title.

Model report for the main defect. Title: Pay a bill returns 500 and creates no payment when the amount has a non-zero decimal part, e.g. 250.75. Environment: web banking 7.3.0 on staging, English UI; Chrome 129 / Windows 11, also reproduced on Firefox 131 / Windows 11; retail customer role, test accounts test-cust-07 and test-cust-12. Severity: Critical. Priority: High. Frequency: Always — all 4 attempts with such an amount failed (runs 2 to 5), and both whole amounts succeeded.

Preconditions: logged in as a retail customer (for example test-cust-07) with a saved biller (Electricity, BLR-ELEC-01) and an account (ACC-TEST-0007) whose balance covers the amount. Steps to reproduce: 1. Open Payments > Pay a bill. 2. Choose the biller Electricity and the account ACC-TEST-0007. 3. Enter 250.75 in Amount. 4. Click "Pay bill".

Expected result: the payment is created, the confirmation page shows a reference number, the payment appears in the account history with the same number, and the balance drops by 250.75 (BILL-3: any amount from 1.00 to 10,000.00 SAR with up to two decimal places). Actual result: a red banner shows "Something went wrong. Please try again later."; POST /api/v2/bill-payments returns 500 with the body {"error":{"code":"INTERNAL","message":"Unexpected error","requestId":"b7e2-41c9"}}; no payment appears in the history and the balance is unchanged.

Evidence: a screenshot of the banner; the response above; the server log line for request id b7e2-41c9, java.lang.NumberFormatException: For input string: "250.75" at bank.payments.AmountMapper.toMinorUnits(AmountMapper.java:58); and the request as a cleaned cURL command: curl 'https://staging.bank.example/api/v2/bill-payments' -H 'accept: application/json' -H 'authorization: Bearer [REDACTED]' -H 'content-type: application/json' --data-raw '{"billerId":"BLR-ELEC-01","fromAccount":"ACC-TEST-0007","amount":"250.75"}'. The bearer token is replaced with [REDACTED] because anyone who can read the tracker could use it while it is valid; the developer puts a token for their own test account in its place. The whole -b option goes too: it carries the session cookie, which is just as usable, and the _ga analytics cookie. In a copied command, cookies can sit in -b rather than in a header, so check both places when you redact. The accept-language, sec-ch-ua-platform and user-agent headers go because the browser adds them on its own and run 4 shows the browser makes no difference. The accept and content-type headers stay because they describe what the request sends and what it expects back; without content-type, curl would label the --data-raw body application/x-www-form-urlencoded. In real work, run the cleaned command once with a fresh token of your own before attaching it: if it still returns the 500, nothing you removed was needed.

Notes: whole amounts succeed (250.00, 1000.00); the failure does not depend on the browser, the account or the biller (runs 4 and 5). The same payment of 250.75 succeeds on UAT, which runs 7.2.4, so 7.3.0 probably introduced it; UAT differs from staging in more than the build, so this is a strong hint rather than proof. No open defect was found for bill-payments or NumberFormatException.

Why Critical: go down the guide’s severity scale from the top. Blocker does not fit, because bill payment still works for whole amounts. Critical does: this is a failed payment, and a customer cannot pay the exact amount of any bill that has a decimal part. Some teams would argue for Blocker because many real bills have a decimal part; with that reasoning either level is defensible, but Major or lower is not, because payments fail. Why High: production still runs 7.2.4, the build on which the same payment succeeds in UAT, so releasing 7.3.0 with this defect would make every bill payment with a non-zero decimal part fail for customers — it must be fixed before release. Had 7.3.0 already been live, the priority would be Highest. As the tester you propose the priority; the triage meeting or the product owner decides it.

Second report. Title: Pay bill button label is cut off in the Arabic UI at 360 px viewport width. Environment: web banking 7.3.0 on staging, Chrome device toolbar at 360 px; the label fits at 412 px, and the English label fits at 360 px. Severity: Minor — cosmetic, the button still works. Priority: Medium, proposed (next sprint), because it is the main action on the page for Arabic users on narrow phones; the product owner may raise it to High before release. Attach screenshots at 360 px and at 412 px and, if you can, confirm on a real phone of that width and name it: device emulation is not the device.

After the fix: confirmation testing repeats the exact steps with 250.75 and 99.50. Add regression checks around the change: whole amounts, the boundaries of BILL-3 (1.00 and 10,000.00 accepted, 0.99 and 10,000.01 rejected), an amount with one decimal place such as 250.5 (accepted) and one with three such as 250.755 (rejected, because BILL-3 allows at most two).

Check your own work. Your report is complete if a developer who never saw the draft could reproduce the failure from your steps alone; the title names the symptom, the place and the condition with no opinion; the steps use one exact amount and leave the other runs to the notes; the expected result cites BILL-3; the actual result quotes the banner, the status code and the request id exactly; the token is redacted and the cookies are gone; severity and priority are set separately, each with a reason; and neither the button defect nor the complaint about the error message’s wording is in this report. In ISTQB terms you observed a failure — the 500 and the missing payment; the report gives the developer what they need to find the defect behind it, and the log line is evidence, not your diagnosis.

Your progress on this topic

Part of the QA Roadmap

Tools mentioned in this guide

Turn rough notes into a complete bug report: steps to reproduce, expected vs actual, environment, severity and attachments checklist.

Bug Analysis AI

Generate clear, searchable bug titles from a messy description with Mutqan AI.

Bug Analysis AI

Get a suggested severity (Blocker / Critical / Major / Minor / Trivial) with justification from Mutqan AI.

Bug Analysis AI

Get a suggested priority (P0–P3) considering business impact, affected users, deadlines and severity, from Mutqan AI.

Bug Analysis AI

Explain stack traces and error messages, identify likely root causes and next debugging steps with Mutqan AI.

Bug Analysis AI

See exactly what your client sends: method, headers, query, body, IP and TLS info.

API Testing

Analyze an API response: structure, field types, nulls, duplicates, sizes and potential data-quality issues.

API Testing

One-stop factory for synthetic test data: users, customers, companies, addresses, numbers, dates and strings in JSON, CSV, SQL or XML.

Test Data

How Mutqan Soft can help

More guides

More guides