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.

Published February 11, 2026 · Updated November 6, 2026 · Author: Spec Coding Editorial Team · Editorial Policy

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

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:

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:

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

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:

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

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 discoverabilityHigh discoverability
High riskDocument 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 riskDocument 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.

LayerEdge caseEvidence before coding
UIUser clicks export twice.Same job_id or disabled duplicate action.
APIRequest exceeds row limit.413 or validation error with clear copy.
JobWorker dies mid-export.Retry policy and partial file cleanup test.
StorageDownload 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

Capture edge cases as first-class requirements, not QA leftovers. That single decision improves delivery quality more than most tooling changes.

Editorial note

This guide covers Edge Case Checklist Before Coding for spec-first engineering teams. Examples are illustrative scenarios, not production code.