In Cypress, what survives the cy.task() boundary between the spec and Node?
answer
- Everything crossing the seam is serialised
- Think JSON.stringify, not structured clone
- Functions and symbols are omitted to null
- A Date arrives as an ISO string
- Methods live on the dropped prototype
basics
~20 sOnly what JSON.stringify() can represent. Cypress serialises both the argument and the returned value, so functions, regular expressions and symbols arrive as null, a Date arrives as a string, and a live handle cannot cross at all.
solid answer
~40 s`cy.task()` sends its argument from the browser to Node and sends the handler's return value back, and both trips go through a JSON serialisation. Anything `JSON.stringify()` cannot represent does not survive: functions, regular expressions and symbols are omitted to `null`, `undefined` properties disappear, a `Date` arrives as an ISO string, a `Map` or `Set` arrives as `{}`, and a class instance arrives as a plain object with its prototype gone. That last point is why you cannot return a live handle — a database client, an open stream, a server — from a task: its methods live on the prototype and are dropped, so the spec receives an object that looks empty. Keep long-lived resources in module scope inside `setupNodeEvents` and return only plain data. Design task payloads as flat JSON on purpose.
code
javascript · 16 lines// cypress/e2e/tenant-provisioning.cy.js
it('provisions a tenant through the Node seam', () => {
cy.task('provisionTenant', {
slug: 'acme',
environment: 'staging',
// a function cannot be serialised: Node receives null here
afterInsert: () => {},
}).then((tenant) => {
// createdAt was a Date in Node; it arrives as an ISO string
expect(tenant.createdAt).to.be.a('string')
// flags was a Map in Node; it arrives as an empty object
expect(tenant.flags).to.deep.equal({})
// plain data survives untouched
expect(tenant.owner.email).to.equal('[email protected]')
})
})go deeper
Know that a value handed to Cypress's cy.task() is serialised, so only plain data — strings, numbers, arrays, plain objects — is guaranteed to arrive on the other side.
Walk through what JSON.stringify() does to a Date, a Map, a class instance and a function, and say exactly what the spec then sees for each.
Show how you spot an assertion that failed because the seam changed a value rather than because the application did, and how you keep Node-side state out of payloads.
Decide what shape every task payload in the suite must take, and whether a revive layer at the seam is worth the drift it can hide.
## Two serialisation hops, not one `cy.task()` is a round trip across the boundary between the browser page your spec runs in and the Node process that loaded your Cypress config. Both directions are serialised: 1. The spec calls `cy.task('provisionTenant', arg)`. `arg` is serialised, sent to Node, and the handler receives the deserialised copy. 2. The handler returns a value (or a promise resolving to one). That value is serialised, sent back, and the command yields the deserialised copy to the next command in the chain. The documented rule for what may cross is exactly the rule for `JSON.stringify()`. Unserialisable types — functions, regular expressions, symbols — are omitted to `null`. Nothing about this is a Cypress quirk you can configure away; it is the price of the two halves being separate runtimes that talk over a channel. ## What JSON.stringify() keeps and what it drops | you send | the other side receives | |---|---| | string, number, boolean, `null` | the same value | | array, plain object | the same structure, copied | | `Date` | an ISO 8601 string | | `Map`, `Set` | `{}` — no own enumerable properties | | `undefined` property | the property is gone | | `NaN`, `Infinity` | `null` | | function, `RegExp`, symbol | `null` | | class instance | a plain object; the prototype is gone | | `Buffer` | `{ type: 'Buffer', data: [...] }` via its own toJSON | The `Date` row is the one that bites most often. A handler that returns `{ createdAt: new Date() }` looks like it returns a date, and the spec's Chai assertion `expect(tenant.createdAt).to.be.a('date')` fails against a perfectly correct application, because the value was changed in transit and not by the code under test. ## Why a handle can never cross Serialisation copies **own enumerable properties**. Methods on a class live on the prototype, so they are never included. A Postgres client, an SSH connection, a Node `fs.ReadStream` or an HTTP server therefore arrives in the spec as an object with some configuration fields and no behaviour at all. There is no option that makes it work, because there is nothing on the browser side that could host the socket the handle wraps. The pattern that does work is to keep the resource where it lives and expose operations, not objects: - Create the client **once in module scope** inside `setupNodeEvents`. That process runs for the whole run, so the connection survives between calls. - Register one task per operation — `provisionTenant`, `suspendEnvironment`, `readAuditRows` — each returning plain data. - Return `null` when there is nothing to hand back, since a handler that returns nothing fails the command. ## Working with the limit rather than around it A few habits keep the seam honest in a multi-tenant admin console suite: - **Normalise at the Node edge.** Convert dates to ISO strings and maps to plain objects in the handler, so the spec's expectations match what actually arrives instead of what the handler happened to build. - **Send one object.** `cy.task(name, arg, options)` carries a single argument, so pack multiple values — tenant slug, environment, feature-flag overrides — into one object and destructure it in Node. - **Do not send behaviour.** A callback in the payload is silently `null` on arrival; the handler cannot call back into the browser, so any decision logic has to live wholly on one side. - **Watch for silently dropped fields.** A property whose value is `undefined` vanishes entirely, so an optional field can be missing rather than empty on the other side. ## Reading a failure that the seam caused Serialisation problems rarely announce themselves. The symptoms are recognisable once you have seen them: - An assertion on a type — Chai's `to.be.a('date')`, JavaScript's `instanceof`, `.size` on a `Map` — fails while the value itself looks right in the Command Log. - A value that is clearly present in the Node handler is `null` in the spec: it was a function, a regular expression or a symbol. - An object round-trips with its data but every method call on it throws `is not a function`: the prototype was dropped. - A task appears to receive nothing for an argument you passed: the argument was `undefined`, or its only fields were. When you suspect the seam, the fastest check is to log the value on the Node side inside the handler and compare it with what the Command Log shows the command yielded. If the two differ, the boundary changed it and the application is innocent — which is the whole point of knowing this rule before you start debugging the app.
- How do you keep a database client alive across several Cypress tasks?Create it once in module scope inside `setupNodeEvents` and let each task handler use it. The client never crosses to the browser, and the Node process lives for the whole run, so the connection survives between calls. Each task returns only plain data about what it did.
- What does a Cypress spec receive if a task returns a Node Buffer?`Buffer` defines its own `toJSON()`, so the spec receives `{ type: 'Buffer', data: [...] }` rather than a buffer. Convert it to a base64 string inside the handler, or, when the bytes come from a file, read them with `cy.readFile(path, null)`, which yields a `Cypress.Buffer` directly.
The seam behaves like a fax rather than a handover: the shape of the thing arrives on the other side, but the thing itself — the open connection, the callback, the class it belonged to — stays where it was.
saying these in an interview costs you the question
- Assumes structured clone, so Maps and Dates survive intact
- Returns a database client from a task and calls its methods
- Passes a callback to cy.task() and expects Node to run it
- Blames the application when a Date arrives as a string
- Thinks the seam deep-copies objects with prototypes attached