Choosing an Asynchronous Strategy in Cypress to Eliminate Flakiness
Learn how to choose the right asynchronous handling strategy in Cypress to eliminate flaky tests. Compare hard sleeps, automatic waiting, and network intercepts, and see a concrete example that uses cy.intercept() to wait for an API response before asserting on the UI.
29 Jul 2025, 13:13 UTC

Problem: Race Conditions in Cypress Tests
When writing end‑to‑end tests with Cypress, the framework’s command queue can lead to race conditions. A test may assert on an element that hasn’t been rendered yet, or it may fire a network request that hasn’t finished before the UI updates. These timing issues manifest as flaky tests that pass intermittently, increasing maintenance overhead.
Decision: Which Async Handling Strategy to Use?
Three primary strategies are available in Cypress for dealing with asynchronous behavior:
- Hard sleeps (cy.wait(ms)) – a fixed pause in execution.
- Automatic waiting and assertions (cy.get().should()) – Cypress retries until a condition is met.
- Network interception and aliasing (cy.intercept() + cy.wait('@alias')) – wait for a specific request to finish.
Each approach has constraints and trade‑offs that influence test stability and speed.
Comparison Table
| Strategy | When to Use | Pros | Cons |
|---|---|---|---|
| Hard sleep (cy.wait(ms)) | Simple, quick to implement. | Deterministic pause; no need to understand the UI state. | Increases test runtime; brittle if timing changes. |
| Automatic waiting (cy.get().should()) | When the UI element’s state is the real indicator. | Reduces flakiness; no manual timing. | May mask underlying performance issues; complex selectors can still fail. |
| Network intercept (cy.intercept() + cy.wait('@alias')) | When the UI depends on a specific API response. | Precise control; tests remain fast. | Requires knowledge of network layer; can’t handle UI updates unrelated to network. |
Trade‑Off Analysis
Hard sleeps are the least recommended because they add unnecessary delay and do not adapt to real application speed. They also make tests sensitive to load changes; if an API call takes longer, the test will fail even though the UI eventually updates.
Automatic waiting leverages Cypress’s built‑in retry‑ability. By asserting on an element’s visibility or text, Cypress will poll until the condition is true or a timeout occurs. This strategy is lightweight and works well for UI‑driven flows, but it can still be fragile if the selector is too specific or if the element appears and disappears quickly.
Network intercepts give the tester explicit control over asynchronous flows. By aliasing a request and waiting for it, you guarantee that the UI has processed the data before proceeding. This is ideal for data‑driven pages where the UI update is tightly coupled to a network response. However, it adds complexity and requires an understanding of the API contract.
Concrete Implementation: Intercepting a Product List API
Below is a minimal test that demonstrates the network‑intercept strategy for a product listing page. The test waits for the API that returns product data before asserting that the products are rendered.
// cypress/integration/products_spec.js
describe('Product list page', () => {
it('renders products after API response', () => {
// 1. Intercept the GET request to /api/products
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: [{ id: 1, name: 'Widget' }, { id: 2, name: 'Gadget' }],
}).as('getProducts');
// 2. Visit the page that triggers the request
cy.visit('/products');
// 3. Wait for the aliased request to finish
cy.wait('@getProducts').its('response.statusCode').should('eq', 200);
// 4. Verify that the UI rendered the products
cy.get('[data-cy=product-item]').should('have.length', 2);
cy.contains('Widget').should('be.visible');
cy.contains('Gadget').should('be.visible');
});
});
**Where to run**: In the Cypress test runner or via the CLI (npx cypress run). **Permissions**: No special permissions are required beyond standard file access. **Placeholders**: Replace "/api/products" and the selector ".product-item" with your actual API endpoint and element. **Expected checks**: The test passes if the API returns a 200 status and the UI displays two products. **Risks**: If the API changes its URL or response format, the intercept will break; maintain the intercept in sync with the backend. **Rollback**: None needed; the test does not alter application state.
Practical Verification Steps
- Run the test and observe the Cypress command log. The log should show cy.intercept, cy.visit, cy.wait('@getProducts'), and the subsequent cy.get commands in order.
- Open the Test Runner’s Command Log pane and confirm that the cy.wait('@getProducts') command is highlighted in green (success) and not red (failure).
- Check the Network tab in the browser dev tools to ensure that the request to /api/products is actually fired and that the response matches the mocked body.
- Run the same test with cy.wait(3000) instead of the intercept. Compare the execution time and stability across multiple runs; the intercept version should be faster and less flaky.
Limitations and When to Combine Strategies
Network intercepts are powerful but not a silver bullet. If a UI element updates independently of a network call (e.g., a countdown timer or a WebSocket push), you may need to combine automatic waiting on the element with network intercepts for the API calls. Also, for very simple pages where a hard sleep of 500 ms reliably covers the delay, a quick cy.wait(500) can be acceptable for prototyping, but should be replaced with a more robust approach before shipping tests.
Takeaway
For reliable, fast Cypress tests, prefer automatic waiting with assertions for pure UI flows and network intercepts with aliasing when the UI state depends on a specific API response. Avoid hard sleeps unless no other option exists. By following this decision guide, you reduce flakiness, improve test speed, and make maintenance easier.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.