skip to content

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

level: seniorimportance: should knowfreq 44%

answer

  1. The injected failure is not simulated
  2. Something in the app never caught it
  3. Cypress watches the application's own errors
  4. The third handler argument is the promise
  5. Suppress one error, never all of them

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.

solid answer

~40 s

`forceNetworkError` is not a polite stub — Cypress destroys the connection, so the dashboard's `fetch()` rejects exactly as it would against an unreachable host. Cypress runs in the same browser as the application and fails the test on any uncaught exception or unhandled promise rejection coming from it, so the spec dies with the app's message before your assertion on the offline banner ever runs. That failure is usually correct: the test has found code with no error path. The honest fix is to give the app one. If the rejection is genuinely expected and handled elsewhere, narrow it with `Cypress.on('uncaught:exception', (err, runnable, promise) => …)` returning `false` for that specific error only — never a blanket `return false`.

code

javascript · 16 lines
javascript
it('keeps the dashboard usable when the forecast host is unreachable', () => {
  // the poller's rejection is expected in this test only
  cy.on('uncaught:exception', (err, runnable, promise) => {
    if (promise && err.message.includes('Failed to fetch')) {
      return false
    }
  })

  cy.intercept('GET', '/api/forecast*', { forceNetworkError: true }).as('getForecast')
  cy.intercept('GET', '/api/stations*', { fixture: 'stations.json' })

  cy.visit('/dashboard')
  cy.wait('@getForecast').should('have.property', 'error')
  cy.get('[data-cy=forecast-offline]').should('be.visible')
  cy.get('[data-cy=station-list] li').should('have.length.greaterThan', 0)
})

go deeper

for a junior

Know that Cypress fails a test when the application under test throws, and that an injected network fault can be the thing that made it throw.

for a middle

Explain the chain: destroyed connection, rejected request, unhandled rejection, failed test — and where in the application the missing catch should go.

for a senior

Show that you treat the failure as a finding first. Be able to justify suppressing a specific expected rejection while keeping every other application exception fatal to the suite.

for a principal

Set the team's rule for exception suppression — where handlers may live, how narrow they must be, and how the suite avoids accumulating blanket handlers that hide regressions.

This failure confuses people because the error message belongs to the application, not to Cypress, so it reads like the test tooling has gone wrong. It has not. The fault you injected worked, and the application had no answer for it. ## The fault is real, not simulated `cy.intercept('GET', '/api/forecast*', { forceNetworkError: true })` destroys the browser connection. From inside the dashboard there is no difference between that and a DNS failure: `fetch()` **rejects** with a `TypeError`, and `XMLHttpRequest` fires its `error` event. Nothing about the rejection is marked as test-generated. If the dashboard's forecast loader has no `catch` — or has one that rethrows, or awaits inside an event handler with no handler above it — the rejection becomes an **unhandled promise rejection** on the application's window. ## Why that ends the spec Cypress executes the spec in the same browser as the application under test and subscribes to the application's error events. Its documented default for the `uncaught:exception` event is to **fail the current test** when an uncaught exception occurs in your application, so the spec fails carrying the app's own message — `TypeError: Failed to fetch`, or whatever the loader threw — and every command queued behind it is skipped, including the assertion on the offline banner. Three details make this readable once you know them: - The event fires with `(err, runnable, promise)`; the third argument is present **only** when the exception originated from an unhandled promise rejection, which is the usual shape for a failed `fetch()`. - The failure is attributed to whichever test was running, even though the code that threw is the application's. - The Command Log still shows the intercepted request in a failed state, with a request snapshot and an error snapshot rather than a response — that entry is the evidence that your route did its job. ## Reading it correctly before you silence it Ask one question before touching any Cypress option: **should the dashboard have survived this?** If the product says the forecast panel shows an offline state when the service is unreachable, then a spec that dies on an unhandled rejection has found a genuine defect. The test is doing exactly what fault injection is for, and the fix belongs in the application, not in the spec. Only when the rejection is genuinely expected — a third-party widget that always throws when offline, a background poller whose failure is deliberately ignored — should the spec suppress it. ## Three ways forward, in order of preference 1. **Add the error path to the application.** The panel catches the rejection and renders its offline state; the spec then passes with no Cypress-side handling at all. 2. **Suppress that one error, narrowly.** Register a handler that returns `false` only for the error you expect, and let everything else keep failing tests: ```js Cypress.on('uncaught:exception', (err, runnable, promise) => { if (promise && err.message.includes('Failed to fetch')) { return false } }) ``` 3. **Scope it to a single test** with `cy.on('uncaught:exception', …)` inside the `it` block, which is unbound automatically when that test ends. Prefer this when only one scenario expects the rejection. What you should not do is put an unconditional `return false` in the support file. That does not make the suite tolerant of injected faults; it makes it blind to every application exception, including the regressions the suite exists to catch. ## Confirming the diagnosis quickly Two pieces of evidence settle it in seconds. First, the failure's message and stack belong to the application's own bundle — a failing Cypress command names the command instead and prints its own explanation. Second, the Command Log entry for the intercepted request sits in a failed state with a request snapshot and an error snapshot rather than a response, and the interception `cy.wait()` would have yielded carries an `error` whose message is `forceNetworkError called` and no `response` property. When both hold, the fault fired as designed and it is the application, not the runner, that stopped the test. ## What the test should assert afterwards Once the rejection is handled, keep the assertions on the application's behaviour rather than on the fault: - the offline or error region is visible and names the failed panel; - the last good forecast, if the product caches one, is still on screen; - a retry affordance exists and is enabled; - unrelated panels — the station list, the alerts feed — still render, because a single failing route should not blank the dashboard. Asserting only `cy.wait('@getForecast').should('have.property', 'error')` proves the fault fired, not that the product handled it. The interception's `error` is worth asserting as a guard, but the user-visible outcome is the point of the test.

  • How do you tell an unhandled promise rejection from a thrown error in the handler?
    The `uncaught:exception` handler is called with `(err, runnable, promise)` and the third argument is present only when the exception came from an unhandled promise rejection. A failed `fetch()` in the application takes that path, so testing for `promise` before matching the message keeps the suppression narrow.
  • When is a spec-level `cy.on('uncaught:exception')` better than a support-file `Cypress.on`?
    When only one scenario expects the rejection. A `cy.on` handler registered inside a test is unbound automatically when that test ends, so it cannot leak into neighbouring tests. A `Cypress.on` handler in the support file applies to every spec, which is right only for an error the whole suite genuinely expects.

saying these in an interview costs you the question

  • Blames Cypress for an error thrown by the application
  • Adds an unconditional return false to the support file
  • Thinks forceNetworkError only affects the test, not the app
  • Raises the command timeout to make the failure go away
  • Asserts only that the interception has an error property