skip to content

Fault and Delay Injection

Making a route fail or crawl on purpose: a transport-level network error, an added delay, a throttled transfer rate, or an error status the application has to render something for.

on this pageshow

explore

questions

5

In Cypress, how does a `cy.intercept()` 500 stub differ from `forceNetworkError`?

level: juniorimportance: must knowfreq 70%

answer

  1. Two layers a request can fail at
  2. One answers badly, one never answers
  3. Does the promise resolve or reject?
  4. StaticResponse statusCode versus a destroyed connection
  5. req.destroy is forceNetworkError in a handler

basics

~20 s

A 500 stub sends a real HTTP response the application can read, so its status and error body are available. forceNetworkError destroys the browser connection instead: no status, no headers, no body, and the app's fetch rejects.

solid answer

~40 s

Both are ways of making a `cy.intercept()` route fail, but they fail at different layers. Passing a StaticResponse such as `{ statusCode: 503, body: { error: 'unavailable' } }` fulfils the request with a complete HTTP exchange — the browser's `fetch()` resolves, `response.ok` is `false`, and any code that reads an error payload runs. Passing `{ forceNetworkError: true }` produces no response at all: Cypress destroys the connection, so the app sees a rejected `fetch()` or an `XMLHttpRequest` error event, exactly as if the host were unreachable. The dynamic equivalents inside a route handler are `req.reply({ statusCode: 503 })` and `req.destroy()`, and `req.destroy()` is implemented as `req.reply({ forceNetworkError: true })`. Pick the one that matches the failure the dashboard is supposed to survive.

code

javascript · 16 lines
javascript
it('shows the offline banner when the forecast host is unreachable', () => {
  cy.intercept('GET', '/api/forecast*', { forceNetworkError: true }).as('getForecast')
  cy.visit('/dashboard')
  cy.wait('@getForecast').should('have.property', 'error')
  cy.get('[data-cy=forecast-offline]').should('be.visible')
})

it('shows the maintenance notice when the forecast service is down', () => {
  cy.intercept('GET', '/api/forecast*', {
    statusCode: 503,
    body: { error: 'forecast service unavailable' },
  }).as('getForecast')
  cy.visit('/dashboard')
  cy.wait('@getForecast').its('response.statusCode').should('eq', 503)
  cy.get('[data-cy=forecast-maintenance]').should('contain', 'unavailable')
})

go deeper

for a junior

Be ready to write both routes from memory: a StaticResponse with an error statusCode, and { forceNetworkError: true }. Know that the first still produces a response and the second produces none.

for a middle

Explain what each fault does to the application's own code — which promise resolves, which rejects, and which branch of the dashboard's error handling each one actually exercises.

for a senior

Show judgment about which failure a feature was written to survive, and be able to say what a suite that only stubs 5xx has left uncovered when a host becomes unreachable in production.

for a principal

Own the convention: which failure modes every network-facing feature must have a test for, and how the team avoids a suite where one cheap stub stands in for every kind of outage.

A weather dashboard has two very different bad days. On one, the forecast service answers — it just answers badly, with `503 Service Unavailable` and an error envelope. On the other, the request never completes at all: the connection dies and the browser rejects before any status exists. Cypress's `cy.intercept()` can stage both, and they are not interchangeable. ## Staging a server error with `statusCode` Passing a **StaticResponse** object as the handler argument to `cy.intercept()` fulfils the request from the test instead of the server: ```js cy.intercept('GET', '/api/forecast', { statusCode: 503, body: { error: 'forecast service unavailable' }, }) ``` The browser sees an ordinary, complete HTTP exchange: a status line, headers, and a body. `fetch()` **resolves**, `response.ok` is `false`, `response.status` is `503`, and whatever the app does with an error payload actually runs. Cypress validates `statusCode` when the route is declared — it must be a number from 100 to 999 — so a typo fails loudly at declaration time rather than mid-test. Inside a route handler the same reply is `req.reply({ statusCode: 503, body: { error: 'forecast service unavailable' } })`. ## Staging a transport failure with `forceNetworkError` `forceNetworkError: true` is a different animal. Cypress destroys the browser connection and sends nothing: ```js cy.intercept('GET', '/api/forecast', { forceNetworkError: true }) ``` There is no status, no header and no body to inspect, because no response was ever produced. As of **Cypress 16**, Chrome, Chromium and Edge intercept on the native browser network, so the fault is delivered as a failed request through the Chrome DevTools Protocol; Firefox, WebKit and Electron still use the legacy proxy path, where the connection is reset. The application cannot tell the difference — in both cases its `fetch()` **rejects** (Chrome reports a `TypeError`) or its `XMLHttpRequest` fires `error`. The dynamic form is `req.destroy()` inside a route handler, and it is literally implemented as `req.reply({ forceNetworkError: true })`. Use it when whether to fail depends on the request: ```js cy.intercept('GET', '/api/stations', (req) => { if (req.query.region === 'offline-region') { req.destroy() } }) ``` Both faults last for the life of the test. A route is not a one-shot: once registered it claims **every** request matching its route matcher until the test ends, so a dashboard that retries its forecast call keeps getting the same destroyed connection. That is usually what an offline-state test wants; when it is not, the route matcher is where you bound it. ## What each one leaves you to assert | | `{ statusCode: 5xx }` | `{ forceNetworkError: true }` | |---|---|---| | App-side outcome | promise resolves, `ok === false` | promise rejects | | Response object | present, with status, headers, body | absent | | Interception yielded by `cy.wait()` | has `response` | has `error`, message `forceNetworkError called` | | Can carry a body or headers | yes | no — must be the only option set | | Models | a service that is up and unhappy | a host that is unreachable | That third row matters in practice. On a forced network error, `cy.wait('@getForecast')` yields an interception with an `error` property and **no** `response` property, so an assertion like `.its('response.statusCode')` has nothing to read. Assert on the error instead: ```js cy.intercept('GET', '/api/forecast', { forceNetworkError: true }).as('getForecast') cy.wait('@getForecast').should('have.property', 'error') ``` The Command Log shows the request in a failed state with a request snapshot and an error snapshot rather than a response. ## Choosing between them 1. **Ask what the application code branches on.** If the dashboard renders a different message for `503` than for `404`, the test needs a real status code; a destroyed connection cannot express one. 2. **Ask which failure the feature was written for.** An offline banner, a "retry" affordance or a cached-forecast fallback is usually triggered by a rejected request, not by a parsed error body — that is the `forceNetworkError` case. 3. **Do not use one to approximate the other.** A `500` stub will not exercise a rejection handler, and a forced network error will not exercise status-code branching. Teams that only ever stub `500` typically discover the gap the first time a DNS failure reaches production. 4. **Check what the browser can even report.** A rejected `fetch()` hands the application a `TypeError` with a deliberately vague message, because browsers do not explain why a request failed. If the dashboard must distinguish "the service said no" from "the service could not be reached", only the status-code path can carry that distinction into the application. One constraint follows directly from the mechanics: because a destroyed connection has nothing to carry a status line or a body, Cypress refuses a StaticResponse that sets `forceNetworkError` alongside `body`, `statusCode` or `headers`. If a test needs both failures, it needs two routes, not one.

  • What does `cy.wait()` give you for a route stubbed with `forceNetworkError`?
    An interception object carrying an `error` whose message is `forceNetworkError called`, and no `response` property at all. Assert with `cy.wait('@getForecast').should('have.property', 'error')`; anything reading `response.statusCode` or `response.body` will find nothing to read.
  • How do you force a network error only for some requests to the same route?
    Use a route handler instead of a StaticResponse and call `req.destroy()` conditionally — for example only when `req.query.region` names the station you want offline. `req.destroy()` is the handler-side equivalent of `{ forceNetworkError: true }`, so requests that fall through the condition are sent on to the real server.

A 5xx stub is the shop answering the phone to say it is closed; forceNetworkError is the line going dead mid-dial. Your code needs a different reaction to each.

saying these in an interview costs you the question

  • Says forceNetworkError returns a response with status 0
  • Thinks a 500 stub makes the app's fetch reject
  • Combines forceNetworkError with a body to 'explain' the failure
  • Uses only 5xx stubs and calls offline behaviour covered
  • Believes req.destroy() cancels the Cypress command instead of the request
open as a page

In a Cypress `cy.intercept()` reply, how do `delay` and `throttleKbps` differ?

level: middleimportance: should knowfreq 52%

basics

~20 s

delay holds the whole response back for a number of milliseconds and then sends it at full speed. throttleKbps leaves the wait unchanged but streams the body slowly. One models latency, the other models bandwidth.

open as a page

A Cypress `cy.intercept()` route with `forceNetworkError` fails the spec with the app's own error. Why?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The injected fault is a real failure, so the application's request genuinely rejects. If nothing in the app catches that rejection it reaches the window, and Cypress fails a test on any uncaught exception from the application under test.

open as a page

How far should a Cypress suite go in throttling every response with `res.setThrottle()`?

level: principalimportance: should knowfreq 30%

basics

~20 s

Keep injected slowness out of the default path. A support-file route that throttles every response buys uniform timing pressure but charges wall-clock time to every spec forever, so put delay on the routes a specific test is about instead.

open as a page

Why does Cypress reject a `cy.intercept()` reply setting both `statusCode` and `forceNetworkError`?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

Because a destroyed connection has nothing to carry a status line, headers or a body. Cypress validates the StaticResponse when the route is declared and throws: forceNetworkError, if passed, must be the only option in the StaticResponse.

open as a page