In Cypress, how does a `cy.intercept()` 500 stub differ from `forceNetworkError`?
answer
- Two layers a request can fail at
- One answers badly, one never answers
- Does the promise resolve or reject?
- StaticResponse statusCode versus a destroyed connection
- req.destroy is forceNetworkError in a handler
basics
~20 sA 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 sBoth 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 linesit('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
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.
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.
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.
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