How to Write Acceptance Criteria That Prevent Bugs
Acceptance criteria prevent bugs when they are written before development starts, cover the unhappy paths as explicitly as the happy one, and are phrased so each criterion has a single, binary pass/fail outcome. Criteria written this way become the shared oracle that developers build to, QA tests against, and automated tests check on every commit — so a defect has to survive three checks that all reference the same source of truth. Vague criteria like “the form should work well” prevent nothing, because nobody can fail them.
What are acceptance criteria?
Acceptance criteria are the specific conditions a feature must satisfy to be accepted as done. They describe observable behavior — what the system does in response to an input or event — not implementation. A good criterion reads like a test case someone could run without asking a follow-up question, and it resolves to either “pass” or “fail,” never “sort of.”
They are not the same as a definition of done. Acceptance criteria are unique to each story and describe correctness; a definition of done is a fixed checklist (tests written, reviewed, deployed) applied to every story. You need both.
Why acceptance criteria prevent bugs
Most bugs are not coding mistakes — they are cases nobody agreed on. A developer builds the path they imagined, QA tests the path they imagined, and the gap between the two ships to users. Acceptance criteria close that gap by making the agreement explicit and written down before anyone touches code. Three specific mechanisms do the work:
- They surface the unhappy paths early. Writing “what happens when the field is empty?” during planning costs a sentence. Discovering it in production costs a bug report, a triage, and a hotfix.
- They give QA a checklist that isn't guesswork. Testers verify against the criteria instead of improvising, so coverage is intentional rather than accidental.
- They become regression tests. A testable criterion maps almost one-to-one onto an automated assertion, so today's agreement becomes tomorrow's guardrail.
How to write acceptance criteria that prevent bugs
Work through these steps with a developer and a tester in the room, before the story is estimated:
- State the happy path first. Write the single most common successful flow as one clear criterion. This anchors everyone on the intended behavior before you branch into edge cases.
- Enumerate the unhappy paths. For every input and action, ask: what if it's empty, invalid, too long, too large, duplicated, or unauthorized? What if the network fails mid-action? Each answer is a criterion. This step catches the bugs that vague stories never mention.
- Make each criterion independently verifiable. One criterion, one observable outcome. If a criterion contains “and” joining two different behaviors, split it — otherwise a half-working feature can pass.
- Remove every unfalsifiable word. Delete “fast,” “intuitive,” “properly,” and “gracefully.” Replace them with the observable proxy: “fast” becomes “responds within 2 seconds,” “gracefully” becomes “shows an error toast and keeps the entered data.”
- Specify the exact expected result. Not “shows an error,” but “shows the message ‘Email already in use’ below the field and disables submit.” The more exact the expectation, the smaller the room for a defect to hide.
Given-When-Then vs. checklist format
Two formats cover almost every case. Use Given-When-Then for behavior that depends on state or triggers:
- Given a user with an unverified email, When they try to publish, Then publishing is blocked and a “Verify your email” banner appears.
Use a plain checklist for static constraints that don't depend on a trigger — validation rules, field limits, and formatting:
- Password must be at least 12 characters.
- Amounts display with two decimal places and a currency symbol.
Don't force everything into Given-When-Then. A list of validation rules reads better as a checklist, and readability matters — criteria nobody reads prevent no bugs.
Common mistakes that let bugs through
- Only the happy path. The most common failure. If every criterion describes success, the feature will break the first time a user does something unexpected.
- Unmeasurable adjectives. “User-friendly” and “performant” can't be failed, so they can't be tested.
- Describing implementation. “Call the /validate endpoint” is a design note, not a criterion. Criteria describe behavior the user can observe, so they survive a refactor.
- Writing them after the code. Criteria written to match what was built rubber-stamp the bugs already there. Their whole value is being written first.
Turn acceptance criteria into tests that don't regress
A criterion prevents a bug once; a test prevents it forever. Because well-written criteria are already binary and behavior-focused, each one maps cleanly onto an end-to-end assertion. Convert the happy path and every unhappy path into automated checks so a future change that breaks the agreed behavior fails the build instead of reaching users.
The friction is maintenance: end-to-end tests break when selectors and copy change, and a suite that cries wolf gets ignored. Klavity AutoSim generates end-to-end tests from your flows and self-heals selectors when the UI shifts, so the criteria you wrote keep guarding the behavior instead of the markup. And where criteria are about whether a real person can actually complete the flow, Sims — AI personas built from customer calls — walk the feature the way users do and surface the gaps your criteria didn't think to cover.
Start small: for your next story, write the unhappy paths before the happy one, make each criterion pass/fail, and automate the three that would hurt most in production. That single habit change prevents more bugs than any tool.
Key takeaways
- Write acceptance criteria before development, not after — they become the test oracle
- Add the unhappy paths: empty, invalid, boundary, error, and permission cases
- Make every criterion independently verifiable with a binary pass/fail outcome
- Turn each criterion into an automated end-to-end check so it can't regress
FAQ
What are acceptance criteria in software?
Acceptance criteria are the specific, testable conditions a feature must satisfy to be considered done. Each criterion is written as a binary pass/fail statement about observable behavior, so QA, developers, and automated tests can all agree on whether the feature works.
What is the difference between acceptance criteria and a definition of done?
Acceptance criteria are specific to one story — they describe what that feature must do. A definition of done is a shared checklist applied to every story, covering things like tests written, code reviewed, and docs updated. You need both: criteria say 'this feature is correct,' the definition of done says 'this work is complete.'
Should QA write acceptance criteria?
The product owner usually drafts them, but QA should review and extend them before development starts. QA's job is to add the unhappy paths — invalid input, error states, boundaries, and permission cases — that product often leaves out. Writing criteria collaboratively before coding is where most bugs are prevented.
Catch bugs the moment a human sees them
Klavity: right-click bug reports, AI personas that review your product, and self-healing tests.
Get started free