In Cypress, what must an `on('task')` handler in `setupNodeEvents` return?
answer
- The Node side has to answer
- Falling off the end is not success
- One value is reserved as a signal
- Cypress separates handled from unhandled
- There is an explicit way to say nothing
basics
~20 sIt must return a value, null, or a promise that resolves to one of those. Returning undefined fails the calling command with a message naming the task, because Cypress reads undefined as "no handler answered this event".
solid answer
~40 sA handler registered with `on('task', { ... })` inside `setupNodeEvents` must resolve to something other than `undefined`. Cypress reserves `undefined` to mean that no handler answered, so a function that does its work and falls off the end fails the test with a message naming the task and repeating the contract: return a value, `null`, or a promise resolving to a value or `null`. When the handler has nothing useful to hand back - resetting a tenant's settings, emptying the downloads folder - end it with an explicit `return null`. When it does have something, return it or return a promise, and Cypress awaits the promise before the spec continues. The handler must also finish: a single invocation is bounded by `taskTimeout`, 60 seconds by default.
code
javascript · 36 linesconst { defineConfig } = require('cypress')
const fs = require('fs')
const {
createTenant,
resetEnvironmentSettings,
} = require('./test-support/admin-console')
module.exports = defineConfig({
e2e: {
taskTimeout: 90000,
setupNodeEvents(on, config) {
on('task', {
'tenant:create'({ name, plan }) {
// returns the created tenant's id to the spec
return createTenant({ name, plan })
},
'settings:resetEnvironment'(environment) {
resetEnvironmentSettings(environment)
// nothing to yield, but the event was handled
return null
},
'downloads:clear'() {
fs.rmSync(config.downloadsFolder, { recursive: true, force: true })
fs.mkdirSync(config.downloadsFolder, { recursive: true })
return null
},
})
return config
},
},
})go deeper
Be ready to write a task handler from memory and say what it hands back. Interviewers watch for the explicit return null on a handler whose only job is a side effect.
Explain why Cypress treats undefined as unhandled rather than as success, and what each of the three different failure messages tells you about which mistake you made.
Expect questions about what happens to a run when a handler never resolves, and about keeping an inherited task registry trustworthy as plugins that register their own tasks are added.
Own the convention: which return shapes tasks in your suite may hand back, who reviews the configuration file, and how a task's contract stays stable once many specs depend on it.
## Where the handler lives `setupNodeEvents(on, config)` is exported from the `e2e` (or `component`) block of your Cypress configuration file. It runs in **Node**, inside a child process Cypress spawns when it opens the project - not in the browser next to your specs. The `on` argument registers listeners; the `config` argument is the resolved configuration for the run. `on('task', { ... })` hands Cypress an object whose **keys are task names** and whose **values are the functions** that run when a spec reaches that name. `task` is in fact the only event whose handler is an object rather than a function; passing a function instead fails the load with a message saying the handler for `task` must be an object. In a multi-tenant admin console the registry is usually a short list of verbs - `tenant:create`, `settings:resetEnvironment`, `flags:readEffective` - each backed by one function. ## The three legal returns A handler must resolve to one of these: - **A value** - whatever the test needs back: the id of the tenant just created, the number of settings rows reset, the list of environment names a tenant has. - **`null`** - the explicit "handled, nothing to hand back". This is the right ending for a handler whose whole purpose is a side effect. - **A promise resolving to a value or `null`** - Cypress awaits it before the spec continues, so database work and file I/O are perfectly normal here. What a handler may **not** resolve to is `undefined`. One that does its work and falls off the end returns `undefined`, and so does an `async` handler with no `return` statement in it. ## Why `undefined` is a failure and not a no-op Cypress reserves `undefined` to mean *nobody answered this event*. Handlers are plain object properties, so without that reservation a misspelled task name and a handler that quietly returned nothing would be indistinguishable. The resulting messages are deliberately different: | what the Node side did | what the spec is told | |---|---| | `on('task', ...)` was never called | the `task` event has not been registered in `setupNodeEvents` | | `task` is registered but no key matches | the task was not handled, followed by every registered key | | the matching handler returned `undefined` | the task returned undefined, plus the source of the handler that ran | | the handler returned a value or `null` | the command passes and the value is yielded onward | The list of registered keys in the second message is the fastest way to spot a typo; the printed handler source in the third is the fastest way to spot the missing `return`. ## Registering more than one task object - Cypress **merges** every `on('task', ...)` call into one registry, so a plugin that exports its own tasks and your own object can both be registered without either being lost. - When two registrations use the **same key**, the last one wins and Cypress warns that there were multiple attempts to register that task and only the last was kept. - That warning is worth reading. A plugin added for an unrelated reason can quietly take over a task name the suite already depends on, and nothing else in the run will say so. ## The handler runs in Node - There is no `cy` and no `Cypress` inside a handler - those objects live in the browser process, so a stray `cy.log()` throws there. - You do have the filesystem, Node's `child_process` and `process.env`, and any module the project has installed, which is the entire point of the seam. - An error thrown inside a handler comes back as a task failure with the Node stack appended, so a rejected promise fails the calling test rather than disappearing. ## How long a handler may take A single invocation is bounded by **`taskTimeout`**, which defaults to **60000 ms**. If the handler has not settled by then Cypress fails the test and prints the handler's source next to the timeout. The bound is per invocation rather than per run, and it is an ordinary configuration value, so a suite whose tenant seeding legitimately takes longer raises it rather than working around it. Handlers that never end at all - a file watcher, a server, anything you would have to interrupt by hand - are the wrong shape for this seam entirely. ## A checklist before you commit a handler 1. **Does every exit path return?** Early returns inside `if` branches are where the missing one usually hides. 2. **Does it end?** If it opens a watcher or starts a long-lived listener, it is not a task. 3. **Is the name an intent?** `settings:resetEnvironment` tells the next reader what the round trip is for; `runSql` tells them only how it happens to work today. 4. **Is the return value worth having?** Handing back the created tenant's id lets the spec assert against what it actually created instead of assuming.
- Two Cypress plugins each register a `tenant:create` task in `setupNodeEvents`. What happens?Cypress merges every `on('task', ...)` object into a single registry, so all the non-colliding keys from both survive. For the shared key the last registration wins, and Cypress prints a warning naming the duplicate task and saying only the last attempt was registered - which is how a plugin added for an unrelated reason can silently replace your own handler.
- What does Cypress report when a spec calls `cy.task('tenant:reset')` and no handler by that name exists?The test fails with a message saying the task was not handled in `setupNodeEvents`, followed by the list of every task key that is registered - which usually makes the typo obvious. If `on('task', ...)` was never called at all, the message is different: it says the `task` event has not been registered. The two errors separate a misspelling from a missing registration.
A task handler is a service desk that has to send something back for every ticket, even a bare acknowledgement. Cypress treats silence as proof the ticket never reached anyone at all.
saying these in an interview costs you the question
- Thinks a handler can do its work and return nothing
- Says returning undefined is the same as returning null
- Believes cy commands are available inside a task handler
- Assumes a handler that never resolves just leaves the test waiting forever