# How to Handle Asynchronous Operations in Cypress: Complete Guide with Examples

> Master asynchronous operations in Cypress. Learn how Cypress's command queue simplifies testing with automatic retries and internal promise management. Get the complete guide with examples.

- Repository: [Cypress.io/cypress](https://github.com/cypress-io/cypress)
- Tags: how-to-guide
- Published: 2026-06-18

---

**Cypress abstracts asynchronous browser activity behind a command queue that automatically retries commands until they succeed, letting you write synchronous-looking test code while the framework manages timing and promises internally.**

Handling asynchronous operations in Cypress requires understanding its unique **command queue** architecture. Unlike standard JavaScript testing frameworks where you manually manage promises, the cypress-io/cypress repository implements an automatic queuing system where commands like `cy.get()` and `cy.visit()` return chainable objects that resolve in sequence. This design eliminates race conditions and the need for explicit `await` keywords in most test scenarios.

## Understanding the Command Queue Architecture

At the core of Cypress's async handling is the **command queue**, a centralized mechanism that sequences every Cypress command. When you call `cy.get()`, `cy.click()`, or `cy.visit()`, the framework enqueues the action rather than executing it immediately. According to the source code in [`packages/driver/src/cy/commands/sessions/index.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cy/commands/sessions/index.ts), each command returns a **Chainable** instance that resolves only after the previous command succeeds.

This architecture provides automatic **retry logic**. If an element isn't present yet, Cypress retries the command until it times out or the condition is met. You write what appears to be synchronous code, but Cypress manages the underlying asynchronous DOM events and network activity behind the scenes.

## Waiting for Network Requests with cy.intercept()

For explicit control over HTTP requests, use **`cy.intercept()`** combined with **`cy.wait()`** and aliases. This pattern is defined in [`packages/network-interception/lib/types/external-types.ts`](https://github.com/cypress-io/cypress/blob/main/packages/network-interception/lib/types/external-types.ts) and demonstrated in [`packages/driver/cypress/e2e/commands/waiting.cy.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/cypress/e2e/commands/waiting.cy.ts).

Instead of arbitrary timeouts, alias your intercepted requests and wait for them deterministically:

```typescript
cy.intercept('GET', '/api/users').as('users')
cy.visit('/dashboard')
cy.wait('@users')
  .its('response.statusCode')
  .should('eq', 200)

```

This approach waits for the actual request completion rather than guessing timing with fixed delays.

## Chaining Custom Async Logic with cy.then()

When you need to run custom asynchronous JavaScript inside a test, wrap it in **`cy.then()`**. As implemented in [`packages/driver/src/cypress/error_messages.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cypress/error_messages.ts), this command accepts a callback that executes only after all preceding commands resolve.

The callback can be synchronous or async. If you return a Promise or use `await`, Cypress waits for resolution before continuing the queue:

```typescript
cy.get('#toggle').click()
cy.then(async () => {
  const result = await cy.window().then(w => w.myAsyncFn())
  expect(result).to.equal('ready')
})

```

**Never** mix native Promises with Cypress commands outside of `cy.then()`. Awaiting a Cypress command directly with `await` bypasses the command queue and causes flaky tests.

## Executing Node.js Code with cy.task()

For operations requiring the Node.js runtime—such as database seeding or file system manipulation—use **`cy.task()`**. This command creates a bridge between the browser and Node processes, as shown in [`packages/driver/src/cy/commands/connectors.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cy/commands/connectors.ts).

When the task function returns a Promise, Cypress automatically waits for its resolution:

```typescript
// cypress.config.ts
export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('task', {
        async seedDatabase() {
          await db.seed()
          return null
        }
      })
      return config
    }
  }
})

// spec file
cy.task('seedDatabase')

```

## Async Support in Plugin Event Hooks

Modern Cypress versions support async functions in plugin event handlers. According to [`cli/CHANGELOG.md`](https://github.com/cypress-io/cypress/blob/main/cli/CHANGELOG.md), events like `before:spec` and `after:run` now accept async callbacks:

```typescript
export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('before:spec', async (spec) => {
        await createTestData(spec.relative)
        return config
      })
    }
  }
})

```

The framework awaits the returned Promise before proceeding with the test run.

## Accessing Test Context with cy.withCtx()

For rare cases requiring direct access to the internal test context, use **`cy.withCtx()`**. As documented in [`guides/e2e-open-testing.md`](https://github.com/cypress-io/cypress/blob/main/guides/e2e-open-testing.md), this command provides the Mocha test instance and other internal objects while remaining async-aware:

```typescript
cy.withCtx(async (ctx) => {
  await ctx.task('seedDatabase')
})

```

## Avoiding Anti-Patterns in Cypress Async Handling

To maintain reliable tests, avoid these common mistakes:

- **Fixed delays**: Never use `cy.wait(2000)` for timing. Instead, use `cy.get().should()` for deterministic retry-ability.
- **Native awaits**: Don't `await` Cypress commands directly. Always chain them or wrap in `cy.then()`.
- **Manual polling**: Let Cypress handle retries automatically rather than writing custom `setTimeout` loops.

## Summary

- Cypress commands automatically enqueue in a **command queue** that manages async operations without manual promise handling
- Use **`cy.intercept()`** with aliases and **`cy.wait('@alias')`** for network request synchronization
- Wrap custom async code in **`cy.then()`** to maintain queue order and prevent flakiness
- Return Promises from **`cy.task()`** callbacks to handle Node.js asynchronous operations
- Plugin events like `before:spec` support async functions as documented in [`cli/CHANGELOG.md`](https://github.com/cypress-io/cypress/blob/main/cli/CHANGELOG.md)
- Avoid fixed timeouts in favor of Cypress's built-in retry logic and deterministic assertions

## Frequently Asked Questions

### Can I use async/await directly with Cypress commands?

No. Using `await` on a Cypress command bypasses the command queue and causes flaky tests. Always wrap async operations in `cy.then()` or use the Promise-returning variants within the Cypress chain. The framework warns about this pattern in [`packages/driver/src/cypress/error_messages.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cypress/error_messages.ts).

### How do I wait for an API call to finish before asserting?

Use `cy.intercept()` to spy on the route and assign an alias with `.as('name')`, then call `cy.wait('@name')`. This waits for the actual request completion rather than an arbitrary time delay, making tests faster and more reliable.

### Why does Cypress retry commands automatically?

Cypress implements automatic retry logic to handle the asynchronous nature of browser rendering and network latency. Commands retry until the assertion passes or a timeout occurs, eliminating the need for manual polling or sleep statements in your test code.

### How do I run asynchronous setup code before a test run?

Use the `setupNodeEvents` function in your Cypress config with async event handlers like `on('before:spec', async ...)`. As documented in [`cli/CHANGELOG.md`](https://github.com/cypress-io/cypress/blob/main/cli/CHANGELOG.md), Cypress will await these promises before starting tests, allowing you to perform async database resets or data generation.