Cypress
async
for loop
JavaScript
testing

Wait for async method in for loop in Cypress

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Plain JavaScript loops and Cypress commands do not behave the same way. If you try to await Cypress commands or drop them into a normal for loop, the test often looks sequential while actually queuing work in a way that surprises you.

Why Plain for Loops Cause Trouble

Cypress commands are not regular promises. They are queued and executed later by the Cypress runner, which gives Cypress its retry behavior and consistent timing model. A JavaScript for loop, on the other hand, runs immediately.

That mismatch is the root of the problem. The loop finishes synchronously, while the Cypress commands it created are still waiting in the queue. If you also mix in your own async functions, the control flow gets harder to reason about.

Prefer cy.wrap(...).each(...)

When you need to process a list item by item, cy.wrap(array).each(...) is usually the cleanest solution. Cypress waits for each callback chain before moving to the next item.

javascript
1const userIds = [101, 102, 103];
2
3cy.wrap(userIds).each((id) => {
4  cy.request(`/api/users/${id}`)
5    .its("status")
6    .should("eq", 200);
7});

This pattern stays inside Cypress's execution model. Each request is queued in order, and the assertions remain visible in the command log.

Use Recursion for More Complex Async Work

Sometimes each iteration needs branching logic, extra assertions, or a helper that calls more Cypress commands. A small recursive helper is often easier to read than forcing everything into one loop.

javascript
1const orderIds = ["a1", "a2", "a3"];
2
3function verifyOrders(ids, index = 0) {
4  if (index >= ids.length) {
5    return;
6  }
7
8  cy.request(`/api/orders/${ids[index]}`)
9    .its("body.state")
10    .should("eq", "processed")
11    .then(() => {
12      verifyOrders(ids, index + 1);
13    });
14}
15
16verifyOrders(orderIds);

Recursion looks unusual at first, but it maps well to the Cypress command queue because the next step is scheduled only after the current chain resolves.

Working with Your Own Async Function

If you have a helper that returns a native promise, bridge it into Cypress instead of trying to await it next to cy commands.

javascript
1function loadToken() {
2  return fetch("/token")
3    .then((response) => response.json())
4    .then((body) => body.token);
5}
6
7cy.then(() => loadToken()).then((token) => {
8  expect(token).to.match(/^tok_/);
9  cy.request({
10    url: "/api/profile",
11    headers: {
12      Authorization: `Bearer ${token}`
13    }
14  }).its("status").should("eq", 200);
15});

cy.then is the handoff point. It lets Cypress wait for your promise and then continue the normal chain.

When a Normal Loop Is Acceptable

A plain loop is fine if it only builds test data before Cypress starts doing work. For example, generating an array of inputs or fixture values in JavaScript is not a problem. The trouble starts when you expect the loop itself to control the timing of Cypress commands.

A good rule is simple: if the loop body contains cy, use a Cypress-native iteration pattern.

Common Pitfalls

  • Using await cy.get(...) is misleading because Cypress commands are not standard promises.
  • Storing yielded values in outer variables and reading them later can create race-like bugs.
  • Returning a native promise while also calling cy commands in the same branch often produces Cypress errors.
  • Using a forEach callback with Cypress commands makes debugging harder because you lose explicit chaining.
  • Trying to force Cypress into plain async JavaScript usually creates brittle tests.

Summary

  • Cypress commands are queued, so normal loop timing does not control them the way you might expect.
  • Use cy.wrap(array).each(...) for straightforward sequential iteration.
  • Use recursion when each step needs more complex Cypress logic.
  • Bridge native promises into the chain with cy.then(...).
  • Keep Cypress control flow inside Cypress patterns to avoid flaky tests.

Course illustration
Course illustration

All Rights Reserved.