QA How-To
How to Use Cypress cy.wrap and cy.then (2026)
Learn cypress cy.wrap and cy.then with command queue rules, subject flow, promises, retry behavior, TypeScript patterns, examples, and interview answers.
22 min read | 2,804 words
TL;DR
cy.wrap(value) starts or restores a Cypress chain around a value, element, or promise. .then(callback) consumes the current subject and can preserve or replace it, but its callback is not retried, so use .should() for assertions that need retryability.
Key Takeaways
- Use cy.wrap() to place a plain value, jQuery element, or promise into the Cypress command chain.
- Use .then() to inspect a yielded subject, transform it, or enqueue commands that depend on it.
- Return a Cypress chain or promise from .then() when its resolved value should become the next subject.
- Prefer .should(callback) for retryable assertions because a .then() callback runs only once.
- Re-query DOM elements after state changes instead of returning raw elements from .then().
- Never expect cy.wrap() to delay a function call that already happened while the command queue was being built.
- Treat Cypress chains as scheduled commands, not JavaScript promises or immediate return values.
The correct way to use cypress cy.wrap and cy.then is to think in terms of subjects moving through Cypress's command queue. cy.wrap(value) introduces a plain JavaScript value, jQuery collection, or promise into that queue. .then(callback) receives the subject yielded by the previous command and lets you inspect it, transform it, or schedule dependent commands.
These APIs solve different parts of the same problem. cy.wrap() bridges ordinary JavaScript into a Cypress chain, while .then() opens a controlled callback at the correct point in that chain. This guide explains the execution model, return rules, retry behavior, TypeScript patterns, and failure modes that matter in production test suites.
TL;DR
| Need | Use | Why |
|---|---|---|
| Put an object or primitive into a Cypress chain | cy.wrap(value) |
It yields that value to later commands |
| Use Cypress commands on a jQuery element from a callback | cy.wrap($element) |
It restores the Cypress chain around the element |
| Wait for an application promise | cy.wrap(promise) |
Cypress waits for fulfillment or fails on rejection |
| Read or transform the current subject once | .then(callback) |
The callback receives the previous yield |
| Retry an assertion until it passes or times out | .should(callback) |
Cypress reruns the assertion callback |
| Run later work only after previous commands | Chain .then(() => ...) |
The callback executes at that queued position |
A compact pattern looks like this:
cy.get('[data-cy=cart-total]')
.invoke('text')
.then((text) => Number(text.replace('#39;, '')))
.then((total) => {
expect(total).to.be.greaterThan(0)
return cy.wrap(total)
})
.should('be.a', 'number')
1. What Cypress cy.wrap and cy.then Actually Do
Cypress commands do not immediately return the application value they appear to target. They enqueue work and return a chainable object. When the runner executes a command, that command yields a subject to the next command. Understanding that distinction prevents most misuse of cy.wrap() and .then().
cy.wrap(subject, options?) is a parent command. Call it from cy, not from an existing subject. It yields the object passed to it. Its supported options include log and timeout. When the subject is a promise, Cypress waits for the promise to resolve and yields the resolved value. A rejected promise fails the test.
.then(options?, callback) is a child command. It must follow a command because it consumes the prior subject. Cypress invokes its callback once when execution reaches that point. The callback's meaningful return value can replace the subject. A returned Cypress chain is awaited, and a returned promise is awaited. If the callback has no meaningful return, Cypress either yields the last Cypress command used inside it or preserves the incoming subject, depending on what the callback did.
The names can be misleading to engineers coming from promises. A Cypress chain is not a native promise, and .then() is a Cypress command with Cypress subject rules. You cannot reliably await cy.get(...) or save a command's returned chain as if it were the element. Read Cypress command queue and retryability before building abstractions that depend on synchronous-looking assignment.
2. The Command Queue and Subject Flow
Test code first builds a queue, then Cypress executes that queue. Ordinary JavaScript outside command callbacks runs while the queue is being constructed. Code inside .then() runs later, after the earlier command has produced its subject. This timing explains why a variable often appears empty outside the callback.
let label = ''
cy.get('[data-cy=account-name]')
.invoke('text')
.then((text) => {
label = text.trim()
expect(label).to.eq('Ada Tester')
})
// This runs while commands are being queued, before the callback above.
expect(label).to.eq('')
Do not fix this by adding a hard wait. Keep the dependent action in the same chain or return a transformed subject:
cy.get('[data-cy=account-name]')
.invoke('text')
.then((text) => text.trim())
.should('eq', 'Ada Tester')
The second version expresses data flow directly. invoke('text') yields the text, .then() returns a trimmed string, and .should() receives that string. No external state is required.
A useful mental model is a conveyor belt. Each command receives the current subject, performs scheduled work, then yields a subject onward. Parent commands such as cy.visit() and cy.wrap() begin a chain. Child commands such as .find() and .then() require the current subject. Dual commands have other rules, but they do not change the core idea: subject flow, not immediate return values, connects Cypress operations.
3. How to Use cy.wrap With Values and Objects
Use cy.wrap() when a plain value needs Cypress assertions, aliases, logging behavior, or later commands. It is useful for domain objects returned by helper code, calculated numbers, arrays, or values captured inside a closure. It does not clone the value. It yields the same reference unless a promise resolves to something else.
type Order = {
id: string
status: 'draft' | 'submitted'
total: number
}
const order: Order = {
id: 'ord-1042',
status: 'submitted',
total: 86.5,
}
cy.wrap(order)
.should('include', { status: 'submitted' })
.its('total')
.should('be.greaterThan', 50)
Wrapping a primitive may improve readability when the value originates in application code:
const calculateTax = (subtotal: number) => subtotal * 0.2
cy.wrap(calculateTax(100), { log: false }).should('eq', 20)
Do not wrap every local constant merely to make the code look like Cypress. Plain Chai assertions are appropriate for immediate synchronous calculations:
const expectedTotal = 40 + 8
expect(expectedTotal).to.eq(48)
Wrap when chain semantics add value. For example, a custom helper may return Cypress.Chainable<Order>, or an element obtained inside .each() must be reintroduced to Cypress before calling click(). For simple deterministic JavaScript, stay in JavaScript. This boundary makes tests easier to read and reduces unnecessary Command Log noise.
4. How to Re-Wrap jQuery Elements Safely
cy.get() yields a jQuery collection, conventionally named with a dollar prefix such as $button. Inside .then(), jQuery methods and properties are immediately available, but Cypress commands are not methods on that object. Use cy.wrap($button) to return to Cypress command behavior.
cy.get('[data-cy=save]').then(($button) => {
const originalText = $button.text().trim()
expect(originalText).to.eq('Save')
cy.wrap($button).click()
})
cy.get('[data-cy=save-status]').should('have.text', 'Saved')
This is appropriate when the callback makes a one-time decision based on the current DOM snapshot. If the action changes or replaces the node, do not return the raw element and continue using it later. The element may be detached. Re-query through a stable selector:
cy.get('[data-cy=toggle-details]').then(($toggle) => {
if ($toggle.attr('aria-expanded') === 'false') {
cy.wrap($toggle).click()
}
})
cy.get('[data-cy=toggle-details]')
.should('have.attr', 'aria-expanded', 'true')
cy.get('[data-cy=details-panel]').should('be.visible')
Notice the separation between action and verification. The second query participates in Cypress retryability and finds the current node. Returning $toggle[0] from .then() would expose a raw DOM element without making it safely reusable. Stable element targeting is equally important, so pair this technique with Cypress data-cy selector strategy.
5. How cy.wrap Handles Promises
When you pass a native promise or compatible thenable to cy.wrap(), Cypress waits for fulfillment and yields the resolved value. This is useful for application services that genuinely return promises. It is not necessary for Cypress commands, because Cypress already schedules and waits for its own chains.
type FeatureState = {
enabled: boolean
rollout: number
}
function loadFeatureState(): Promise<FeatureState> {
return window.fetch('/api/features/checkout-v2').then((response) => {
if (!response.ok) throw new Error('Feature request failed')
return response.json() as Promise<FeatureState>
})
}
cy.wrap(loadFeatureState(), { timeout: 10000 })
.should('include', { enabled: true })
.its('rollout')
.should('be.within', 0, 100)
The function invocation happens immediately when test code builds the queue. Wrapping the returned promise makes Cypress wait for its settlement, but it does not postpone calling loadFeatureState(). That detail matters when the call must occur after a UI action.
cy.get('[data-cy=refresh-feature]').click().then(() => {
return cy.wrap(loadFeatureState())
}).its('enabled').should('eq', true)
Here the function is invoked inside the later callback, so it starts after the click command completes. A rejected promise fails the test automatically. Set a command-specific timeout only when the operation has a justified service-level expectation. Do not enlarge timeouts to hide a promise that never settles. Also avoid starting multiple promises before Cypress reaches them if their order matters. Cypress can only sequence work that is created at the correct point in its command chain.
6. Cypress cy.wrap and cy.then Return Rules
The return rules of .then() determine the next subject. This is the area most likely to produce subtle bugs, so use explicit returns whenever the next command depends on the callback's result.
| Callback outcome | Next subject |
|---|---|
| Returns a plain non-null value | That value |
| Returns a Cypress chain | Final yield of that chain after it resolves |
| Returns a promise | Fulfilled value of that promise |
| Returns nothing and runs Cypress commands | Yield of the last Cypress command in the callback |
| Returns nothing and runs no Cypress commands | Previous subject remains |
| Returns a raw DOM element | Unsafe for later commands, re-query instead |
cy.get('[data-cy=price]')
.invoke('text')
.then((rawPrice) => {
return Number(rawPrice.replace('#39;, ''))
})
.should('eq', 24.99)
Returning a command chain supports dependent lookup:
cy.get('[data-cy=selected-product-id]').invoke('text').then((id) => {
return cy.request<{ name: string }>(`/api/products/${id.trim()}`)
}).its('body.name').should('eq', 'Mechanical Keyboard')
Be careful when a callback both enqueues Cypress commands and returns an immediate synchronous value. Current Cypress detects ambiguous mixing and throws an error. If commands are necessary, return their chain or place the synchronous transformation in a nested .then() that contains no Cypress commands. Explicit chain ownership is clearer than relying on the implicit last command.
Returning null or undefined does not become a null subject. Cypress applies its preservation rules. If you truly need to assert a nullable domain value, wrap an object containing it, such as cy.wrap({ value: null }).its('value').should('be.null').
7. cy.then vs cy.should for Assertions
Use .then() for one-time work and .should(callback) for assertions that may become true after the application updates. Cypress reruns a .should() callback as part of query retryability. A .then() callback executes once and its assertions execute once.
// Retryable: the callback can run again with an updated query result.
cy.get('[data-cy=upload-progress]').should(($progress) => {
const value = Number($progress.attr('value'))
expect(value).to.eq(100)
})
// One-time transformation after the value is stable.
cy.get('[data-cy=invoice-total]')
.should('not.have.text', '')
.invoke('text')
.then((text) => Number(text.replace(/[$,]/g, '')))
.should('be.greaterThan', 0)
A .should(callback) must be idempotent because Cypress can call it repeatedly. Do not click, send a request, mutate application state, or increment external counters inside that callback. Put side effects in .then() after a retryable readiness assertion.
cy.get('[data-cy=export-status]')
.should('have.text', 'Ready')
.then(() => {
cy.get('[data-cy=download-export]').click()
})
This division is intentional. .should() waits for readiness, and .then() schedules a single dependent action. The assertion guidance is not an absolute ban on expect() inside .then(). One-time assertions on stable values are valid. The question is whether the value can legitimately change within the timeout. If yes, make the query and assertion retryable.
8. Conditional Logic Without Creating Flaky Tests
.then() makes conditional branches easy, but a callback only sees the DOM snapshot available at that moment. Conditional testing is reliable only when the condition has already settled and both branches represent acceptable, deterministic states. It cannot safely guess whether a slow element might appear later.
cy.get('body').then(($body) => {
const hasOnboarding = $body.find('[data-cy=onboarding-dialog]').length > 0
if (hasOnboarding) {
cy.get('[data-cy=onboarding-dialog]')
.find('[data-cy=skip-onboarding]')
.click()
}
})
cy.get('[data-cy=dashboard]').should('be.visible')
This may be suitable if server fixtures synchronously determine whether onboarding is rendered before the body query completes. It is unreliable if the dialog arrives after an unpredictable request. In that case, control the server state before the visit, intercept the defining request, or make separate tests for onboarded and new users.
Prefer explicit state setup:
cy.request('POST', '/test-support/users/u-102/onboarding', {
completed: false,
})
cy.visit('/dashboard')
cy.get('[data-cy=onboarding-dialog]').should('be.visible')
Use .then() to branch on a stable response, cookie, configuration value, or element attribute. Do not use it as a substitute for waiting. If a branch can silently skip the behavior under test, the test may pass without proving anything. Each branch should end in an observable assertion, and important variants deserve separate named tests.
9. TypeScript Patterns and Custom Helpers
TypeScript can express the subject returned by a reusable helper. Return the Cypress chain instead of hiding it behind a function that returns void. Consumers can then continue chaining and the compiler can help preserve the data contract.
type UserProfile = {
id: string
email: string
plan: 'free' | 'team'
}
function getCurrentUser(): Cypress.Chainable<UserProfile> {
return cy.request<UserProfile>('/api/me').then(({ body }) => body)
}
getCurrentUser()
.should('include', { plan: 'team' })
.its('email')
.should('match', /@example[.]test$/)
A transformation helper can accept a subject and wrap its result:
function currencyValue(text: string): Cypress.Chainable<number> {
const amount = Number(text.replace(/[^0-9.]/g, ''))
return cy.wrap(amount, { log: false })
}
cy.get('[data-cy=order-total]')
.invoke('text')
.then(currencyValue)
.should('eq', 129.95)
Do not annotate jQuery elements as raw HTMLElement when Cypress actually yields JQuery<HTMLElement>. Use the correct type in custom command declarations. Avoid adding a custom command for a one-line wrap or transformation. Commands add global API surface and can obscure the chain. A typed function is often easier to discover, test, and refactor.
When a custom command accepts a previous subject, follow the documented prevSubject contract and return the resulting chain. Never use a type assertion solely to silence an incorrect yield. Trace the true subject through each return path, particularly callbacks with conditional branches.
10. Debugging Subject and Timing Failures
Classify a failure before changing code. An undefined property often means the prior command yielded a different shape. A detached element suggests a stale DOM reference. An assertion that passed locally but failed in CI may be non-retryable. Work that started too early usually means a function was invoked outside the intended callback.
Use small, safe inspections:
cy.request('/api/orders/ord-1042').then((response) => {
Cypress.log({
name: 'order-shape',
message: Object.keys(response.body).join(', '),
})
expect(response.status).to.eq(200)
return response.body
}).its('status').should('eq', 'submitted')
Do not log tokens, passwords, full customer records, or private response bodies. Inspect types, keys, status codes, and safe identifiers. You can also temporarily add .debug() or use the browser console from an open-mode run.
Read the chain from left to right and write down each expected yield. Verify whether the callback returns a value, a command, a promise, or nothing. Check whether an element is replaced after a click. Confirm that a promise-producing function is invoked inside the correct queued callback. Replace fixed waits with observable assertions.
For network-dependent values, an alias can make timing explicit: register cy.intercept() before the action, trigger the action, wait for the alias, then transform its response. See Cypress intercept practical examples for request matching and response assertions.
11. A Review Checklist for Cypress cy.wrap and cy.then
Review a complicated chain by annotating its execution contract. For every command, name the incoming subject, the outgoing subject, and whether Cypress can retry the relevant query and assertion. For every callback, record whether it returns a plain value, Cypress chain, promise, null, or nothing. This short exercise usually reveals an accidental implicit yield or a callback that starts work too early.
Use these review questions:
- Does any ordinary JavaScript read a value before the queued command produces it?
- Does a promise-producing function start inside the callback that establishes its order?
- Is a changing assertion attached to a retryable query through
should()? - Are side effects outside callbacks that Cypress may rerun?
- Does a state-changing action require a fresh DOM query afterward?
- Is the callback's return explicit when later commands depend on it?
- Could a direct
expect()replace an unnecessary wrap?
Consider extracting a long chain only after its subjects are correct. A helper should reduce domain repetition, not hide control flow. Give it a business name and declare its final Cypress.Chainable<T> type. Keep timeouts close to the operation that genuinely needs them.
Finally, run the failure path. Reject the promise, delay the UI update, return an unexpected response shape, and trigger a re-render. A robust chain should fail with a message that points to the violated contract. If it merely times out on an unrelated later selector, strengthen the earlier readiness assertion or response validation.
Interview Questions and Answers
Q: What is the difference between cy.wrap() and .then()?
cy.wrap() is a parent command that yields the value passed to it, or the resolved value of a promise. .then() is a child command that receives the previous subject and runs its callback once. Wrap introduces or restores a value to a chain, while then consumes and optionally transforms the subject.
Q: Why can you not assign cy.get() to a variable and use the element immediately?
cy.get() returns a Cypress chainable while its lookup is queued for later execution. It does not synchronously return the matched DOM element. Use a closure through .then(), an alias, or continued chaining to work with the yielded subject at the correct time.
Q: Does .then() retry failed assertions?
No. The callback runs once, so assertions inside it are one-time assertions. Use a retryable query followed by .should(), or use a .should(callback) when the assertion needs to observe a changing value.
Q: What happens when a .then() callback returns a Cypress command?
Cypress waits for that returned chain to finish. Its final yield becomes the next subject. Explicitly returning the chain is the clearest pattern when later commands depend on its result.
Q: Is cy.wrap(promise) the same as delaying the promise-producing function?
No. JavaScript invokes the function before its returned promise is passed to cy.wrap(). To delay invocation until earlier Cypress work completes, call the function inside a later .then() callback and return or wrap its promise there.
Q: Why is returning a DOM element from .then() unsafe?
The application can replace or detach that node after state changes, and the callback itself is not retried. Re-query the element with a stable Cypress query before acting on it. If an immediate action is required inside the callback, use cy.wrap($element) there.
Q: When would you use .then() instead of .should(callback)?
Use .then() for a single transformation, dependent command, branch on settled state, or intentional side effect. Use .should(callback) for pure assertions that should retry. The decision is based on timing and retry semantics, not syntax preference.
Common Mistakes
- Treating Cypress chains as promises and trying to use
awaitor synchronous assignment without a supported adapter. - Writing an assertion in
.then()when the application value still needs retry time. - Putting clicks, requests, or other side effects inside a retryable
.should(callback). - Calling an asynchronous function before
cy.wrap()and expecting wrap to postpone the invocation. - Returning raw DOM elements from
.then()and continuing after the application re-renders. - Omitting
returnwhen the next command depends on a nested Cypress chain or promise. - Mixing queued Cypress commands and an immediate synchronous return in the same callback.
- Depending on a mutable outer variable before the callback that sets it has executed.
- Wrapping every constant even when a direct Chai assertion is clearer.
- Using conditional DOM checks before the application state has become deterministic.
- Increasing timeouts without identifying which subject, promise, or assertion is actually waiting.
Conclusion
Cypress cy.wrap and cy.then become predictable once you follow subject flow. Use cy.wrap() to introduce a value, re-wrap a jQuery element for an immediate Cypress action, or wait for a real promise. Use .then() to access the current yield at the correct queued time and explicitly return whatever should flow next.
For reliable tests, reserve retryable .should() callbacks for changing assertions, re-query the DOM after state changes, and invoke dependent async work inside the callback that establishes its order. Review one flaky chain by labeling each yield and return, then simplify until the data path is obvious.
Interview Questions and Answers
Explain cy.wrap versus cy.then in Cypress.
cy.wrap() is a parent command that introduces a value or promise result into a Cypress chain. .then() is a child command that consumes the previous subject and executes once. I use wrap to bridge plain JavaScript or jQuery values into Cypress, and then to inspect or transform subject flow.
How does returning a value from .then() affect the chain?
A non-null synchronous value becomes the next subject. A returned Cypress chain or promise is awaited and its resolved yield becomes the subject. If there is no meaningful return, Cypress applies its last-command or subject-preservation rules.
Why is .should(callback) usually better than .then() for changing UI assertions?
.should(callback) can be rerun while its linked query retries, so it can observe the UI reaching the expected state. .then() runs once. I keep a should callback pure because Cypress may execute it multiple times.
How do you sequence a promise-producing function after a click?
I chain .then() after the click, invoke the function inside that callback, and return its promise or cy.wrap() it. Calling the function before the callback starts it too early, even if its returned promise is later wrapped.
What is wrong with returning a raw DOM node from .then()?
The node can become detached when the framework re-renders, and the callback does not retry. I either perform an immediate action through cy.wrap($element) or re-query the current node with a stable selector.
How would you debug an incorrect subject in a Cypress chain?
I write down what each command yields, inspect the callback's return path, and log only safe type or shape information. I check for implicit last-command yields, detached nodes, early promise invocation, and non-retryable assertions before changing timeouts.
Can Cypress commands be awaited like native promises?
Not as a general pattern. Cypress commands are scheduled chainables with their own queue and subject model. I use Cypress chaining, closures, aliases, and documented adapters rather than treating a chain as a native promise.
Frequently Asked Questions
What does cy.wrap do in Cypress?
cy.wrap() yields the value passed to it so later Cypress commands can use that value as their subject. If the value is a promise, Cypress waits for fulfillment and yields the resolved result.
What does cy.then do in Cypress?
.then() receives the subject yielded by the previous command and runs its callback once. A returned value, Cypress chain, or promise can become the next subject.
Does cy.then retry?
No. The callback and assertions inside it run once. Use .should() or .should(callback) when an assertion needs Cypress retryability.
Can cy.wrap wait for a promise?
Yes. cy.wrap(promise) waits for the promise to resolve and yields its value, while a rejection fails the test. The promise-producing function is still invoked before wrap receives the promise unless you call it inside a queued callback.
Why should I use cy.wrap on a jQuery element?
A jQuery element received in .then() supports jQuery operations but not Cypress commands. cy.wrap($element) places it back into a Cypress chain so you can click, type, or assert through Cypress.
What happens if cy.then returns nothing?
If the callback runs no Cypress commands, the incoming subject usually continues. If it runs Cypress commands, the last command's yield becomes the next subject. Explicit returns are clearer when later steps depend on the result.
Should I use cy.then or cy.should for assertions?
Use .should() for assertions that may need to retry while the application settles. Use .then() for one-time transformations, dependent work, and assertions on values that are already stable.