Fixing Pact Provider State Setup Failures
Symptom: provider verification fails on an interaction that works perfectly in production.
Verifying a pact between web-app and orders-api
a request for order ord_42
given an order ord_42 exists
returns a response which
has status code 200 (FAILED - 1)
Failures:
1) Expected status 200 but was 404
The endpoint is fine. The database is empty. This guide is part of Consumer-Driven Contracts with Pact and covers the provider state layer, where the large majority of verification failures actually live.
Root Cause: The State Handler Is the Test Fixture
A pact interaction is a request, an expected response, and a given clause naming the world in which that exchange makes sense. Pact replays the request; you are responsible for creating the world. When the handler is missing, mis-named, or incomplete, the provider answers correctly for an empty database and the verification fails.
Fix 1: Confirm the Handler Exists and Matches Exactly
State names are matched as strings. A trailing full stop, a capital letter, or a rephrasing on the consumer side means no handler runs — and most verifiers treat a missing handler as a warning rather than an error, so the run continues and fails later.
// provider/verify.pact.ts
await new Verifier({
provider: "orders-api",
providerBaseUrl: "http://127.0.0.1:3000",
// Fail loudly rather than skipping silently.
stateHandlers: {
"an order ord_42 exists": async () => { /* … */ },
},
}).verifyProvider();
# List the states the pact actually asks for — the authoritative source.
jq -r '.interactions[].providerStates[]?.name' pacts/web-app-orders-api.json | sort -u
# an order ord_42 exists
# an order ord_42 exists and has been refunded
# no order exists with the id ord_missing
Diffing that list against your handler keys is a five-second check that resolves most failures immediately.
Fix 2: Create the Whole World, Not Just the Row
An order needs a customer; a refund needs a payment. A handler that inserts only the headline record leaves the endpoint returning a 500 from a foreign-key lookup, which looks nothing like a setup problem.
stateHandlers: {
"an order ord_42 exists": async () => {
// Everything the endpoint touches, in dependency order.
await db.customers.insert({ id: "cus_1", email: "pact@example.test" });
await db.orders.insert({
id: "ord_42", customer_id: "cus_1", status: "paid",
total_minor: 4200, currency: "EUR", created_at: "2026-07-01T10:00:00Z",
});
await db.lineItems.insert({ order_id: "ord_42", sku: "SKU-1", quantity: 1,
unit_price_minor: 4200 });
},
}
Why fixed values: the consumer recorded ord_42, so the handler must produce exactly that identifier. Random or auto-generated ids fail the replay, because the request path contains the recorded one.
Fix 3: Parameterise Instead of Duplicating
Twenty states differing only by identifier is a maintenance problem and a sign the consumer is over-specifying. Pact supports state parameters:
// Consumer side — the state carries its own data.
.given("an order exists", { id: "ord_42", status: "paid", total_minor: 4200 })
// Provider side — one handler covers every variant.
stateHandlers: {
"an order exists": async (setup: boolean, params: Record<string, unknown>) => {
if (!setup) { // teardown call
await db.orders.delete({ id: params.id as string });
return;
}
await db.customers.upsert({ id: "cus_1", email: "pact@example.test" });
await db.orders.insert({
id: params.id as string,
customer_id: "cus_1",
status: (params.status as string) ?? "paid",
total_minor: (params.total_minor as number) ?? 1000,
currency: "EUR",
});
},
}
The setup boolean is the teardown hook: the verifier calls the handler twice per interaction, once to create and once to clean up.
Fix 4: Tear Down, or Reset
Passing individually and failing together is always state leakage. Two approaches work; mixing them does not.
// Approach A — each handler cleans up after itself.
stateHandlers: {
"an order exists": async (setup, params) => {
if (setup) await seedOrder(params);
else await db.orders.delete({ id: params.id as string });
},
},
// Approach B — reset everything between interactions.
beforeEach: async () => {
await db.truncateAll(); // fast on a test database, brutally reliable
},
Approach B is simpler and usually fast enough. Approach A is necessary when the provider shares a database with something you cannot truncate.
Verification
$ npm run test:pact:provider
Verifying a pact between web-app and orders-api
a request for order ord_42
given an order exists (id=ord_42, status=paid)
returns a response which
has status code 200 (OK)
has a matching body (OK)
a request for a missing order
given no order exists with the id ord_missing
returns a response which
has status code 404 (OK)
2 interactions, 0 failures
Two checks catch the remaining classes of problem:
# Every state the pact requires has a handler.
comm -23 \
<(jq -r '.interactions[].providerStates[]?.name' pacts/*.json | sort -u) \
<(node -e 'console.log(Object.keys(require("./provider/states").handlers).join("\n"))' | sort)
# (empty — no unhandled states)
# Interactions are order-independent.
npm run test:pact:provider -- --random-order
# 2 interactions, 0 failures
The random-order run is the one that exposes leakage: a suite that only passes in its authored order has interactions depending on each other’s residue.
Edge Cases and Caveats
- Multiple states on one interaction. Pact allows several
givenclauses; handlers run in the order recorded. A handler that assumes it runs first will break when a consumer adds an earlier state. - States that need time. “an order placed 40 days ago” cannot be produced by waiting. Inject a clock the handler can set, or write the timestamp directly — never make the test sleep.
- Shared environments. Verifying against a shared staging database makes states unreliable by construction, because other traffic mutates them. Verify against a process you control with its own datastore.
- Authentication interacts with state. When a state also implies a credential — “a caller without the write scope” — keep the credential concern in the request filter and the data concern in the state handler, as described in contract testing authenticated endpoints.
- State names as a contract. Once published, a state name is part of the agreement between the two teams. Renaming it breaks verification for every consumer using it, so treat a rename with the same care as a field rename.
Designing States That Do Not Rot
The failures above are the acute ones. The chronic version is a state set that grows until nobody can tell which handlers are still needed, and every provider change breaks three of them for reasons unrelated to the change.
Name states after the world, never after the mechanism. “an order exists” survives a migration from one datastore to another; “a row exists in the orders table” does not. Consumers should be able to write a state name without knowing anything about the provider’s implementation, and that constraint is what keeps handlers replaceable.
Keep the number of states close to the number of distinct situations. Two consumers needing “an order exists” should share one state, not have one each. Where a consumer’s state differs only in a value, that value is a parameter.
Make the handler idempotent. A handler that fails when its data already exists breaks on a re-run after a partial failure, and re-runs are common in CI. Upsert rather than insert, so running the same state twice is harmless.
Delete handlers when the interaction goes. A state handler with no interaction referencing it is dead code that still runs, still touches the database and still breaks on migrations. Compare the handler keys against the states the pacts actually request, and fail the build when a handler is orphaned — the same comparison that catches a missing one.
Together these keep the state set roughly constant in size as the number of interactions grows, which is the difference between a suite that stays useful and one a team eventually deletes.
A Checklist for the Next Failure
When a verification fails, work down this list before reading any application code. It resolves the large majority of cases in under five minutes: confirm the state name in the failure matches a registered handler exactly, confirm the handler creates every row the endpoint reads and not only the headline record, confirm the identifiers are the fixed values the consumer recorded rather than generated ones, and confirm the previous interaction cleaned up after itself. Only when all four pass is the failure likely to be a genuine contract break in the endpoint.
Frequently Asked Questions
Why does verification return 404 when the endpoint exists?
Because the provider state handler did not create the resource the interaction assumes. The request replays against an empty database and the provider correctly answers 404 — the failure is in the setup, not in the endpoint.
Should a provider state name describe an action or a fact?
A fact. “an order ord_42 exists” describes a world the provider must create. “create an order” describes a step, which couples the consumer’s wording to the provider’s implementation and breaks when the implementation changes.
How do I avoid one state per identifier?
Use state parameters. One handler for “an order exists” taking an id parameter replaces twenty near-identical states, and the consumer supplies the id it recorded.
Why do tests pass individually and fail together?
State leaking between interactions. Each state handler must leave the system as it found it, or the next interaction inherits rows it did not create. Add a teardown, or reset the datastore between interactions.
What if a state cannot be created through the database?
Create it through the provider’s own API or service layer inside the handler. Reaching into the database is faster but encodes the schema into the test, so it breaks on a migration that the endpoint itself survives.