Edge Case Checklist Before Coding
Most post-release defects happen outside the happy path. Edge-case planning is the cheapest way to reduce incidents before code review and QA cycles begin.
Quick answer
A complete edge-case list covers invalid input, duplicates, boundary limits, concurrency, permissions, timeout behavior, and rollback safety. Use it during spec review — not after implementation.
Core edge-case categories
- Input validation: null, empty, malformed, or out-of-range values.
- Data uniqueness: duplicate submissions and idempotent retries.
- Boundary constraints: max length, max count, pagination edges.
- Race conditions: concurrent updates and stale-read decisions.
- State transitions: actions attempted from the wrong status, such as cancelling twice.
- Time: expirations, daylight-saving changes, and time-zone boundaries.
- Authorization: role-specific visibility and action limits.
- Dependency failure: timeout, retry, circuit-breaker behavior.
Edge-case matrix example
# Edge Case Matrix: Order Create Flow - request body missing customerId expected: 400 validation error, no DB insert - same idempotencyKey repeated expected: return original 201 response body - concurrent stock reservation on last inventory item expected: one success, one 409 conflict - user role = viewer tries create action expected: 403 forbidden, audit log entry - payment provider timeout expected: retry with backoff, eventual failure -> 503 with traceId
How to write an edge case QA can test
An edge case is testable when three things are concrete: the starting state and input, the trigger, and the expected observable outcome. If any of them is a verb without an object, or an adjective standing in for a number, it is not a test case yet. The check is simple: can QA write the test without asking anyone a question? "Handle edge cases appropriately" fails it immediately.
Give every edge case the same fields, whether the spec uses Given/When/Then or a fixture list:
Edge case: Pay Now clicked twice on an unpaid invoice
- Start state: user has one unpaid invoice; the saved card is expired
- Input: payment retry with the same invoice_id within 60 seconds
- Action: user clicks Pay Now twice
- Expected: one provider charge attempt; the second request
returns the existing attempt_id
- Evidence: ledger has one pending payment row; UI shows one banner
- Fixture: duplicate-pay-now.json
Naming the fixture matters more than it looks. Once the file lives in the test repository, API, UI, log, and database assertions can reuse the same input, and the edge case stops being a sentence in a document.
The same discipline turns a vague line into something QA can run without a meeting:
Before: - Network failure should be handled gracefully. After: - Given a logged-in buyer has a cart with one $49 item And the payment provider accepts the charge but the app receives a 30s timeout When the buyer taps Pay again within 2 minutes Then the app reuses the original idempotency key And shows "payment pending" instead of charging again And support can find the pending payment by order_id in the audit log
Two structural rules keep the list executable. First, describe behavior, not implementation: "the backend retries three times" stays; "the backend uses exponential backoff with a Bloom filter cache" goes. Second, every edge case ends in something a tester can observe. Internal-only behavior such as "log a warning" counts only if the spec names that log entry as the observable.
Put edge cases in their own section below the happy-path acceptance criteria, grouped by the categories on this page and written in the same format as the happy path. The acceptance criteria examples guide covers that format in detail.
Input boundaries: six values to try on every field
Every input has up to six values worth testing: empty, one, the maximum allowed, the maximum plus one, negative, and Unicode or special characters. Not all of them apply to every field, but assume they might until the spec says otherwise. For a display name capped at 50 characters:
- Given the display name field (max 50 chars) When the user submits "" (empty) Then the form shows "Name is required" inline and submit is disabled - Given the display name field When the user submits 51 characters Then the request is rejected with 422 and "Name too long (max 50)" - Given the display name field When the user submits 50 characters including emoji (4-byte Unicode) Then the request succeeds and the name renders correctly in the user list
The emoji case is the one most often skipped. Clients, APIs, and databases can count length as code points, UTF-16 units, or bytes, so the spec has to say which count the limit uses. The file-size cases in the high-risk examples below apply the same exactly-at-limit and limit-plus-one pattern.
State transitions, concurrency, and time
Wrong-state transitions
Anything with a status field has transitions that can go wrong. Specs usually describe the happy path (active → cancelled) and skip re-activation, double cancellation, and cancellation while a change is pending. For each transition, write what happens when it is attempted from the wrong state. "User cancels an already-cancelled account" is a real scenario and will turn up early in rollout.
Concurrent writes
If two users can act on the same record, the feature has a concurrency rule whether the spec states one or not. The test is what happens when both submit within 100ms of each other. Answer three questions:
- Who wins: last write, first write, or a conflict returned to both?
- What does the losing user see: a silent overwrite, an error message, or a merge screen?
- How does the losing user's screen catch up: immediate refresh, polling, or only after a reload?
QA cannot test "last write wins with a conflict banner" unless the spec says that is the rule.
Time-based edges
Time edges stay invisible until the clock crosses them:
- Timeouts. The external call takes 30 seconds instead of 300ms. Does the user see an error, a retry, or a spinner that never stops?
- Expirations. A session, token, or trial expires mid-action. That is a different case from expiring before the action starts.
- Daylight saving. Does a job scheduled inside the repeated or skipped hour run twice when clocks fall back, or not at all when they spring forward?
- Time zones. What does "end of day" mean for users in UTC+13 and UTC−12 on the same calendar date?
Three error paths for every happy path
Every happy-path criterion has at least three error paths: validation, authorization, and downstream dependency failure. The spec should show what the user sees for each, with a status code and exact copy:
- Given the user is authenticated but lacks the "admin" role
When they request /admin/users
Then the response is 403 with body {"error": "forbidden", "required_role": "admin"}
And the UI shows "You don't have access to this page" with a link back
- Given the downstream billing service returns 503
When the user submits checkout
Then the UI shows "Payment is temporarily unavailable, please retry in a minute"
And the request is retried up to 3 times with exponential backoff server-side
And no order record is created until at least one retry succeeds
Write failures as scenes, not nouns. "Interrupted upload" cannot be tested. "A 200 MB file on a slow connection, the network drops at 80%, and the browser refreshes" can, and it forces a product decision: resumable upload, a clear failure with retry, or an explicit loss of progress. Any of the three is testable once the spec picks one.
Review questions for teams
- Which edge cases are explicitly accepted as out-of-scope?
- What behavior is expected under duplicate or retried requests?
- Are conflicting writes deterministic and testable?
- Can monitoring detect these failure modes in production?
If the team cannot answer these questions before coding, the risk has been deferred, not removed.
When QA should send the spec back
QA has good grounds to return a spec for edits when it sees:
- Edge cases written as prose instead of criteria. "Handle expired sessions gracefully" is a wish, not a test.
- Error paths without status codes or exact messages. "Show an error" cannot be asserted on.
- A whole category missing, such as no concurrency section on a feature with shared state.
- Adjectives standing in for numbers: "fast," "reasonable," "significant."
- Setup left implicit: data, permissions, feature flags, timing, network state, and existing records.
Before the spec is approved, run a five-minute test: pick one edge case and write the test as QA would. If you can state the exact input, trigger, and assertion without opening the code or asking anyone, it is ready. If not, the spec owes another sentence. Edge cases that still need a product decision go back into the spec as open questions, so QA does not discover the decision during test execution.
Definition of done for edge-case coverage
- Each high-risk edge case has a mapped test case.
- Expected behavior is documented for retries and partial failures.
- Rollout has alerts for edge-case regressions.
- Rollback does not create inconsistent data states.
High-risk domain examples
The order-create matrix above shows the pattern. These domain-specific examples cover three areas where edge cases are most costly when discovered late: payment processing, authentication, and file ingestion.
# Payment processing edge cases - amount = 0.00 -> reject with validation error, no gateway call - currency mismatch between cart and card -> 400, no charge - gateway timeout after charge initiated -> record ambiguous state, trigger reconciliation job, return 503 with retryable flag - duplicate payment attempt within 10s -> return original receipt, no second charge # Authentication edge cases - expired token used after 1ms grace window -> 401, not 403 - valid token for deactivated account -> 403 with account_suspended code - simultaneous login from 3 sessions -> oldest session invalidated, new session issued, audit log records all three events - password reset link reuse after first click -> 410 Gone # File upload edge cases - file size exactly at limit (10MB) -> accepted - file size at limit + 1 byte -> 413 with clear size message - file type spoofed (PDF extension, executable content) -> rejected by content inspection, not extension check - upload interrupted mid-transfer -> partial file discarded, no record created
Payment edge cases that reach production typically result in double charges or unreconciled transactions — both expensive to reverse. Authentication edge cases that are undocumented produce inconsistent security behavior that is invisible until a breach audit. File edge cases are the most common source of security vulnerabilities when content inspection is absent.
How to prioritize: risk × discoverability
Not every edge case requires the same level of spec detail. A practical prioritization uses two axes: the consequence if the case goes wrong (risk), and how easily QA or monitoring would detect it in production (discoverability). High risk combined with low discoverability requires an explicit spec entry and a mapped test case before implementation begins.
| Low discoverability | High discoverability | |
|---|---|---|
| High risk | Document and test before coding: payment conflicts, concurrent writes, permission boundary failures, data corruption paths. | Document and add monitoring: performance degradation, third-party outages, schema validation failures. |
| Low risk | Document as out-of-scope with rationale: cosmetic rendering edge cases, non-default locale formatting. | Note but do not block: minor validation errors with clear user messaging. |
The output of this prioritization is not a complete list of every possible edge case — that is impossible and counterproductive. It is a prioritized list that the team agrees on before coding, with each item classified by who owns the test case and when it must be resolved.
Which edge cases block release
Risk and discoverability decide how much spec detail an edge case needs. A second decision belongs in the spec too: what happens when its test fails. Write the rule down so QA does not have to negotiate priority during test execution.
Edge-case priority rule Blocks release: - money movement can duplicate or disappear - a permission boundary can be bypassed - user data can be overwritten without warning - a retry can create a second irreversible action - support cannot identify the failed state Can ship as documented risk: - cosmetic copy mismatch - rare timing issue with no data loss - non-critical notification delayed, with a clear retry path - admin-only workflow with a manual workaround
Teams that want a middle tier can label the remaining cases should-test and exploratory. Whatever the labels, a failing test should tell the team at once whether to stop the release, accept the risk, or move the issue to the next iteration.
Edge cases across system layers
Edge cases cluster differently depending on the layer of the system they affect. A single user action can expose distinct edge cases at the API layer, the service layer, the database layer, and the background job layer simultaneously. The spec should identify which layer each edge case lives in, so the correct engineer owns the test.
At the API layer, watch for malformed request bodies, missing required headers, oversized payloads, and unexpected content types. At the service layer, the common cases are dependency unavailability, circuit-breaker state, retry storm behavior, and cache invalidation timing. At the database layer, look for unique constraint violations, foreign key failures on delete, transaction isolation level effects, and migration rollback impact on existing data. At the background job layer, consider job queue backup under load, duplicate execution from failed acknowledgment, poison message handling, and dead-letter queue behavior.
A common gap is edge cases at the job layer. Because jobs run asynchronously, their failures are invisible to the user at request time — which is exactly why they need explicit spec coverage. An undocumented job failure mode that silently drops records is more damaging than a synchronous error that a user can retry.
Real review scenario: large report export
Use this checklist when a feature looks simple but can fail across layers. A large CSV export has UI edge cases, API timeout behavior, background job failures, storage permissions, and cleanup rules.
| Layer | Edge case | Evidence before coding |
|---|---|---|
| UI | User clicks export twice. | Same job_id or disabled duplicate action. |
| API | Request exceeds row limit. | 413 or validation error with clear copy. |
| Job | Worker dies mid-export. | Retry policy and partial file cleanup test. |
| Storage | Download URL expires. | Expired-link state and regenerate path. |
Bad edge-case lists try to cover everything. Useful lists name the few failures that are expensive, hard to discover, or likely to be missed by an AI-generated implementation.
Use these templates
Related articles
Editorial note
This guide covers Edge Case Checklist Before Coding for spec-first engineering teams. Examples are illustrative scenarios, not production code.
- Author details: Spec Coding Editorial Team
- Editorial policy: How we review and update articles
- Corrections: Contact the editor