โ† All guides

Test loading & error states without touching your code

Every app has a happy path, a loading state, and an error state. You built all three โ€” but you've only ever seen one of them, because your dev API answers in 40 ms and never fails. Then production ships, a request takes four seconds on hotel wifi, and your "loading state" turns out to be a blank white screen.

The usual workarounds are all bad: dev-tools network throttling slows down everything including your hot reload; conditional if (FAKE_ERROR) throw code sneaks into commits; mock service workers need per-test setup. Mockbird bakes failure into the API itself: two query params, on every endpoint, no configuration.

ParamWhat it does
?mock_delay=2000Holds the response for N ms (max 5000) before answering normally.
?mock_status=500Returns that HTTP status (400โ€“599) with a JSON error body instead of data.

Try them right now against the public demo โ€” no signup, no setup:

curl "https://HOST/m/demo/products?mock_delay=3000"   # answers after 3s
curl -i "https://HOST/m/demo/products?mock_status=503" # โ†’ 503 {"error":"simulated 503 error (mock_status)"}

1. Does your skeleton loader actually show?

Point one fetch at a delayed URL and watch your UI honestly render its loading state:

const API = "https://HOST/m/demo";

// during development, add the flag:
fetch(`${API}/products?mock_delay=2500`)
  .then(r => r.json())
  .then(render);

Things you'll catch in the first minute: layout shift when the data pops in, spinners that never appear because a cache short-circuits them, skeletons sized for 3 rows when the response has 30. Because the delay happens server-side, everything else โ€” your bundler, your other requests โ€” stays fast.

2. Race conditions, the classic one

Type-ahead search has a famous bug: the response for "an" arrives after the response for "ana" and overwrites it. You can't reproduce that against a fast local API. You can with a delay flag:

// fire a slow request, then a fast one
fetch(`${API}/products?search=an&mock_delay=3000`).then(r => r.json()).then(show);
fetch(`${API}/products?search=ana`).then(r => r.json()).then(show);
// wrong-order arrival guaranteed. Does your AbortController / stale-check cope?

3. Does your error boundary catch it?

Every status from 400 to 599 is available. The ones worth wiring into a checklist:

fetch(`${API}/products?mock_status=500`)  // generic failure โ†’ error boundary / toast?
fetch(`${API}/products?mock_status=401`)  // expired session โ†’ redirect to login?
fetch(`${API}/products?mock_status=403`)  // forbidden โ†’ useful message, not a crash?
fetch(`${API}/products?mock_status=404`)  // gone โ†’ empty state, not spinner-forever?
fetch(`${API}/products?mock_status=429`)  // rate limited โ†’ back off and retry?

The error body is JSON ({"error":"simulated 500 error (mock_status)"}) with CORS headers intact, so your response-parsing code runs exactly as it would on a real failure โ€” including the branch where you try to .json() an error response.

4. Slow and failing

Real outages are rarely instant. Combine the flags to simulate a timeout-then-error, the case that breaks naive retry loops:

fetch(`${API}/products?mock_delay=4000&mock_status=502`)
// 4 seconds of suspense, then a 502. Does retry #2 also wait politely?

4ยฝ. Chaos mode: test retry & backoff for real

mock_status fails every request โ€” great for error boundaries, useless for retry logic (a retry against a always-500 endpoint never succeeds). mock_chaos fails a random fraction:

// 30% of requests randomly return 500/502/503/504/429 โ€” the rest succeed
fetch(`${API}/products?mock_chaos=0.3`)

// pick your poison: only 503s + 429s (test your Retry-After handling)
fetch(`${API}/products?mock_chaos=0.5&mock_chaos_status=503,429`)

// realistic network: 100โ€“1500ms random latency on top
fetch(`${API}/products?mock_chaos=0.2&mock_jitter=100-1500`)

Injected failures carry an x-mockbird-chaos: injected response header, so your test can count how many were chaos vs. real. Two guarantees that make this safe to point retry logic at: a chaos-failed write is never applied (like a server that died before processing โ€” so you can verify re-sending is safe), and with mock_chaos under 1 a retry loop with enough attempts always converges. A quick Playwright example:

test('retries survive a flaky backend', async ({ page }) => {
  await page.route('**/api/**', route => {
    const u = new URL(route.request().url());
    u.searchParams.set('mock_chaos', '0.4');       // 40% failure rate
    route.continue({ url: u.toString() });
  });
  await page.goto('/dashboard');
  await expect(page.getByRole('table')).toBeVisible();  // still renders โ€” retries did their job
});

Hosted chaos engineering like this is an enterprise-tier feature at WireMock Cloud. Here it's a query param.

5. Use it in end-to-end tests

Because the failure lives in the URL, E2E tests don't need any interception setup. One Playwright example:

test("shows error toast on server failure", async ({ page }) => {
  await page.route("**/products*", route =>
    route.continue({ url: route.request().url() + "&mock_status=500" }));
  await page.goto("/catalog");
  await expect(page.getByText("Something went wrong")).toBeVisible();
});

One line rewrites the request to its failing twin โ€” the app code under test is byte-identical to production. The same trick works in Cypress with cy.intercept.

6. Hand it to QA

The best part: these flags need zero technical setup, so anyone can use them. Paste a URL with ?mock_status=500 into a bug report and the repro is the link itself. QA can walk the whole error matrix with nothing but a browser address bar.

7. Check what your app actually sent

When a test fails and you're not sure whether the request even went out (or went out with the wrong body), open the request inspector in the dashboard: the last 50 requests per project are logged with method, path, query string, response status, Origin, and the body of writes. Tick "live" and watch requests stream in while you click through your app โ€” no proxy, no dev-tools archaeology.

8. Reset the data itself between tests

Latency and status flags make failures deterministic; snapshots make the data deterministic too. Save the project state as baseline once, then a single POST โ€ฆ/snapshots/baseline/restore in your beforeEach puts every record back exactly as saved โ€” ids included. Full Playwright/Cypress setup: deterministic test data guide.

9. Bonus: test the events, not just the requests

If your app also consumes webhooks, the same project can generate them: configure a webhook URL and Mockbird fires an HMAC-signed POST every time a record is created, updated or deleted โ€” so you can rehearse your consumer against real, signed deliveries before the production event source exists. Full walkthrough: send test webhooks to any URL.

The examples above use the shared demo project, which resets daily. For your own data, create a project (no signup needed) on the landing page โ€” the flags work on every endpoint, including nested routes and single records. Docs โ†’