skip to content

In Cypress, why does a value returned from a .each() callback never reach the next command?

level: seniorimportance: nice to knowfreq 24%

answer

  1. The loop that never changes the subject
  2. Callback gets value, index, collection
  3. Only one return value means anything
  4. An empty collection runs nothing at all
  5. .spread() is the one that replaces

basics

~20 s

Cypress's .each() always yields back the array-like subject it was given, whatever the callback returns, and it deliberately breaks the subject link to any chain the callback started. Only returning exactly false means something: it ends the loop early.

solid answer

~40 s

`.each(cb)` iterates an array-like subject - a jQuery collection, an array, anything with a `length` - calling the callback with `(value, index, collection)` and wrapping DOM elements as jQuery objects for you. When the loop finishes, Cypress yields **the original subject**, and it does so on purpose: it breaks the subject links that Cypress commands inside the callback would otherwise create, so nothing the loop produced leaks into the outer chain. That makes `.each()` a side-effect step, never a transform - there is no `map` here. Two consequences bite in real suites. Returning exactly `false` stops the loop early and is the only return value with any meaning. And when the subject is empty the callback never runs at all, so a `.each()` full of assertions passes without checking a thing.

go deeper

for a junior

Know that .each() takes a callback and runs it once per item, and that DOM elements arrive as jQuery objects. Wrap one with cy.wrap() if you need a Cypress command inside the loop.

for a middle

Explain that the callback's return value is discarded and the original subject is yielded on, with false as the single exception that ends the loop. Contrast it with .then() and .spread().

for a senior

An interviewer at this level expects the empty-collection story: a .each() of assertions that goes green because there was nothing to iterate, and the length assertion you put in front of it so that never happens again.

for a principal

Own the review rule. Any loop that asserts must be preceded by a claim about how many items should exist, otherwise the suite quietly stops covering the case it was written for.

## What `.each()` yields `.each(cb)` walks an array-like subject and calls your callback once per item with three arguments: the value, its zero-based index, and the original collection. DOM elements are wrapped as jQuery objects on the way in, so `$el` in the callback behaves like anything else jQuery hands you. When the loop is done, `.each()` yields **the subject it started with**. Not a mapped collection, not the last callback result, not `undefined` - the same array-like value, every time. Cypress makes that explicit internally: after the loop it severs the subject links that any Cypress commands started inside the callback would otherwise have created, precisely so those inner chains cannot decide the outer subject. `.then()` has the opposite default, which is why the two commands feel so different in a chain that looks almost identical. ```javascript cy.get('[data-cy=loan-row]') .should('have.length.at.least', 1) .each(($row) => { return $row.find('[data-cy=due-date]').text() }) .then(($rows) => { // $rows is still the jQuery collection of loan rows }) ``` ## The only return value that matters Returning exactly `false` from the callback stops the iteration - Cypress checks for the boolean and skips every remaining item. Nothing else is special: - `return 'something else'` is discarded. - `return null` does **not** break the loop; it is discarded like any other value. - `return undefined` is what most callbacks do implicitly and is equally discarded. - Returning a promise **is** honoured in one respect: `.each()` awaits it before starting the next iteration, which is how you keep an asynchronous per-row step serial rather than racing. ## The empty-collection trap This is the part that costs a real suite real money. If the subject has a length of zero, the callback never runs, and `.each()` yields the subject and moves on without complaint. A loop like this checks nothing at all when the catalogue search returns no results: ```javascript cy.get('[data-cy=loan-row]').each(($row) => { cy.wrap($row).find('[data-cy=due-date]').should('not.be.empty') }) ``` It is green on the day the search endpoint breaks and returns an empty list, because there is nothing to iterate and therefore nothing to fail. The habit that fixes it is one line: 1. Assert the shape of the collection first - `.should('have.length.at.least', 1)`, or the exact count when the fixture is known. 2. Then iterate. Now an empty result fails at the length assertion, loudly, before the loop. 3. If the count is genuinely variable, capture it and assert on it in the following `.then()` rather than trusting the loop to have run. The Cypress docs add a related caution: because `.each()` is not a query, it is unsafe to chain commands after it that act or assert on DOM elements the application may have re-rendered during the loop. Reading the yielded collection in a `.then()` is fine; re-querying with a Cypress query is safer than reusing it. ## Collecting values out of a loop Since the return value is discarded, the way to get data out is a closure variable declared outside the chain and read after it. Inside the callback you are holding a jQuery object, so jQuery's own `find()` and `text()` are the right tools for reading: ```javascript const dueDates = [] cy.get('[data-cy=loan-row]') .should('have.length', 3) .each(($row) => { dueDates.push($row.find('[data-cy=due-date]').text()) }) .then(() => { expect(dueDates).to.deep.equal([...dueDates].sort()) }) ``` The `.then()` runs after the loop has finished, so the array is fully populated by the time it is read. Pushing inside the callback and reading in a later step is the whole pattern. ## `.each()` next to its neighbours | command | subject it expects | what it yields | |---|---|---| | `.each(cb)` | array-like | the same subject, always | | `.spread(cb)` | array-like | the callback's return value, or the previous subject when it returns nothing | | `.then(cb)` | anything | the callback's return value, or the previous subject when it returns nothing | `.spread()` is the one people reach for when they discover `.each()` will not transform: it is `.then()` for an array-like subject, expanded into separate callback arguments, and its return value does replace the subject. Use `.each()` when you want to *do* something per item, `.spread()` or `.then()` when you want to *produce* something from the collection as a whole. ## A checklist for any `.each()` in review - **Is there a length claim in front of it?** Without one the loop can pass by doing nothing. - **Does the callback return anything meaningful?** If it does, the author probably wanted `.spread()` or a closure array, because `.each()` throws that value away. - **Does anything after it act on elements the loop may have re-rendered?** Re-query with a Cypress query rather than reusing the yielded collection. - **Is there a `return null` pretending to be a break?** Only `false` ends the loop. - **Does a Cypress command inside the callback act on the raw item?** Wrap it first - `cy.wrap($row).find(...)` - so the action is queued and logged.

  • What does Cypress's .each() do when its callback returns a promise?
    It awaits the promise before starting the next iteration, so the loop stays serial rather than racing. That is how you sequence an asynchronous step per row. The resolved value is still discarded, because `.each()` yields the original collection regardless of what the callback produced.
  • How does Cypress's .spread() differ from .each()?
    `.spread()` is `.then()` for an array-like subject: it expands the collection into separate callback arguments and its return value replaces the subject, exactly as `.then()` does. `.each()` calls the callback once per item and always yields the collection back unchanged.

saying these in an interview costs you the question

  • Expects .each() to transform the collection like map
  • Returns null from the callback to break the loop
  • Runs .each() assertions without checking the length first
  • Thinks the iterations run in parallel
  • Treats a green .each() as proof the loop ran