Resource library

QA How-To

How to Use Cypress cy.task (2026)

Learn cypress cy.task setup with secure Node handlers, serializable inputs, async returns, database patterns, timeouts, debugging, and practical 2026 guidance.

20 min read | 3,179 words

TL;DR

Register a Node handler in setupNodeEvents, then call it with cy.task('event', argument). The handler must complete and return data, a promise, or null. Keep tasks narrow, serializable, bounded, and secure because they execute with the Cypress Node process permissions.

Key Takeaways

  • Register named task handlers with on('task', handlers) inside setupNodeEvents.
  • Pass one JSON-serializable argument and return serializable data, a promise, or null.
  • Use cy.task only for work that genuinely requires Node capabilities.
  • Add operation-level deadlines because Cypress blocks while a task is running.
  • Expose narrow domain operations instead of arbitrary SQL, shell, URL, or filesystem access.
  • Keep substantial Node logic in plain modules that can be unit tested without Cypress.

The cypress cy.task command runs named code in Cypress's Node process and returns the handler's serializable result to the browser-side test. Register handlers in setupNodeEvents, call them with cy.task('name', argument), and always return a value, a promise, or null. Returning undefined makes the command fail intentionally.

Use a task only when the work truly needs Node capabilities, such as controlled database setup, filesystem inspection, terminal logging, or an approved external process. Ordinary browser actions, HTTP setup, fixtures, and file reads already covered by Cypress commands should stay in the test runner.

TL;DR

// cypress.config.ts
import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('task', {
        log(message: string) {
          console.log(message)
          return null
        },
      })
      return config
    },
  },
})
// cypress/e2e/example.cy.ts
cy.task('log', 'Starting checkout test')
Rule Practical meaning
One argument Pass an object when the task needs several values
JSON-serializable boundary Do not pass functions, symbols, or regular expressions
Explicit result Return data, a promise, or null, never accidental undefined
Blocking command Keep work bounded because Cypress waits for completion
Trusted Node process Validate inputs and never expose unrestricted shell or filesystem access

1. What Cypress cy.task Does

A Cypress test executes in a browser-related runner, while setupNodeEvents executes in Node. cy.task() is the named message bridge between those contexts. The spec sends an event name and one serializable argument. Cypress finds the matching handler registered for the task plugin event, waits for it to finish, then yields the returned value.

This boundary solves problems that browser JavaScript cannot or should not solve. A task can query a test database through a controlled repository, examine a download with Node libraries, maintain process state across spec files, write a terminal log, or coordinate several server-side requests. It is not a faster form of every Cypress command. Crossing processes adds serialization, waiting, error handling, and security concerns.

The basic syntax is:

cy.task('eventName')
cy.task('eventName', argument)
cy.task('eventName', argument, { timeout: 20_000 })

cy.task() yields the handler result, so continue with .then(), .its(), or a one-time assertion. The command requires the handler to end. Long-lived watchers, servers, and subscriptions are inappropriate because Cypress cannot continue while the task remains pending.

For browser-originated API setup, Cypress cy.request for API testing is usually simpler. Choose a task only when Node access or a Node-only dependency is part of the requirement.

2. Register Tasks in setupNodeEvents

Register an object of named handlers with on('task', handlers) inside the testing type configuration. This TypeScript example uses only Node built-ins and can run after Cypress is installed.

// cypress.config.ts
import { defineConfig } from 'cypress'
import { readdir } from 'node:fs/promises'

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    setupNodeEvents(on, config) {
      on('task', {
        terminalLog(message: string) {
          console.log(`[e2e] ${message}`)
          return null
        },

        async countFiles(directory: string) {
          const entries = await readdir(directory, { withFileTypes: true })
          return entries.filter((entry) => entry.isFile()).length
        },
      })

      return config
    },
  },
})

The corresponding spec calls each event by its exact property name:

it('reports generated downloads', () => {
  cy.task('terminalLog', 'Checking the downloads directory')
  cy.task<number>('countFiles', 'cypress/downloads').then((count) => {
    expect(count).to.be.at.least(0)
  })
})

Returning config is useful when setup modifies configuration and makes the hook's outcome explicit. The task registration itself does not need to return anything. Place substantial task implementations in imported Node modules rather than allowing cypress.config.ts to become an untestable collection of database, filesystem, and parsing logic.

Keep registration deterministic. Do not discover handler names from an uncontrolled directory at runtime or register different capabilities based on whichever spec happens to run first. Cypress configuration should expose the same task contract for every worker using that testing type. When component and end-to-end testing share handlers, import one reviewed handler object into both configurations rather than duplicating implementations.

Fail fast during configuration when a mandatory Node dependency or safe environment setting is absent. A clear startup message is better than a later task error that looks like application failure. Optional capabilities can validate their own prerequisites when called, but their event names should still be registered consistently.

3. Design the Serializable Argument and Result

cy.task() accepts one argument. Package several inputs into a plain object and validate it in Node. Values cross a JSON serialization boundary, so functions, regular expressions, symbols, circular objects, and other non-serializable structures cannot retain their behavior. Send a regular-expression source string and flags if a task truly needs to construct one.

type FindTextInput = {
  file: string
  pattern: string
  flags?: string
}

type FindTextResult = {
  matched: boolean
  match?: string
}

// spec
cy.task<FindTextResult>('findText', {
  file: 'cypress/downloads/report.txt',
  pattern: 'Status: (PASS|FAIL)',
  flags: 'i',
} satisfies FindTextInput).its('matched').should('eq', true)

Return plain data with a stable schema. Convert database-specific numeric or date types deliberately if the driver produces values that do not serialize as the consumer expects. Avoid returning an entire driver result, HTTP client response, error object, or file buffer when the test needs only three fields. A narrow result improves security, readability, and compatibility.

If a handler has nothing meaningful to return, return null. Cypress treats undefined, including a promise resolved with undefined, as an error. That behavior catches misspelled event names and handlers that accidentally fall through without completing their contract.

4. Handle Async Work and Timeouts

A task handler may return a promise. Cypress waits for it and yields the resolved value. A rejection fails the command and carries the task error into the test output.

type HealthResult = {
  ok: boolean
  status: number
}

on('task', {
  async serviceHealth(url: string): Promise<HealthResult> {
    const response = await fetch(url, { signal: AbortSignal.timeout(5_000) })
    return { ok: response.ok, status: response.status }
  },
})
cy.task<HealthResult>('serviceHealth', 'http://127.0.0.1:4000/health', {
  timeout: 10_000,
}).should('deep.equal', { ok: true, status: 200 })

The task timeout should exceed the handler's own operation deadline so the handler can throw a specific diagnostic error first. A single huge Cypress timeout only converts a clear dependency problem into a slow generic failure. Put deadlines on network calls, database queries, process execution, and locks inside the task.

Assertions chained to cy.task() run once rather than retrying the task. If an eventually consistent system must be polled, implement bounded polling deliberately in Node or use a Cypress-side helper that repeats an idempotent read. Never rely on .should() to rerun a mutating seed task.

Cypress blocks subsequent commands while a task runs. A task that takes minutes multiplies suite duration across workers. Prefer a purpose-built setup endpoint, a pre-test pipeline step, or an isolated fixture snapshot for heavy environment preparation.

5. Organize Task Modules Without Name Collisions

As the suite grows, export handler objects by capability and combine them in configuration. Distinct names such as downloads:inspectPdf and db:seedAccount reveal the boundary and reduce accidental collisions.

// cypress/tasks/downloadTasks.ts
import { stat } from 'node:fs/promises'

export const downloadTasks = {
  async 'downloads:fileSize'(file: string) {
    const details = await stat(file)
    return details.size
  },
}
// cypress.config.ts
import { defineConfig } from 'cypress'
import { downloadTasks } from './cypress/tasks/downloadTasks'

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('task', {
        ...downloadTasks,
        'log:info'(message: string) {
          console.log(message)
          return null
        },
      })
      return config
    },
  },
})

Task registrations can be merged when on('task', ...) is called more than once, but duplicate names are replaced. An explicit combined object makes precedence visible during review. Treat a duplicate event name as a design error unless an override is documented.

Keep I/O logic in plain exported functions and wrap it with thin task adapters. Plain Node functions can receive focused unit tests without launching Cypress. The adapter validates serialized input, invokes the function, and shapes the result for the spec.

Avoid a single generic task factory that accepts a module name and method to invoke. Although that reduces registration lines, it removes the allowlist, weakens types, and makes repository searches less useful. Explicit event names are inexpensive documentation. They also let CI logs and ownership rules identify the database, report, mail, or download capability involved in a failure.

If two modules propose the same name, resolve the collision during configuration rather than relying on object spread order. A small unit test can assert that the combined key set is unique before Cypress starts.

6. Compare cy.task With Cypress Alternatives

Use the narrowest built-in mechanism that meets the requirement. This table prevents many unnecessary tasks.

Requirement Preferred tool Why
Read a file known to exist cy.readFile() Built-in retry and Command Log behavior
Load stable fixture data cy.fixture() Clear test-data intent and encoding support
Write a test artifact cy.writeFile() No custom Node handler needed
Call an application API cy.request() Direct HTTP semantics and response object
Run a shell command that exits cy.exec() Explicit command API and result fields
Read an optional file cy.task() Node can check existence without a failed read command
Query a database through a controlled adapter cy.task() Requires Node driver and credentials
Observe or stub browser traffic cy.intercept() Operates at the browser network boundary

Do not use tasks as generic wrappers around cy.readFile(), cy.request(), or cy.exec() merely to hide syntax. Wrappers can also conceal logs, timeouts, and failure context.

The Cypress cy.request examples guide covers direct service setup. Cypress cy.intercept fundamentals covers browser traffic. Tasks complement those tools by reaching Node-only resources, not by replacing Cypress's command set.

7. Seed and Clean Data Safely

Database tasks are common because the browser should not hold database credentials. Expose domain operations rather than raw SQL. A task named db:seedOrder can validate input, use parameterized queries inside a repository, and return the created identifiers. A task named db:query that accepts arbitrary SQL gives every spec broad and difficult-to-audit power.

type SeedOrderInput = {
  runId: string
  accountId: string
  sku: string
}

type SeedOrderResult = {
  orderId: string
  status: 'draft'
}

on('task', {
  async 'db:seedOrder'(input: SeedOrderInput): Promise<SeedOrderResult> {
    assertSeedOrderInput(input)
    return orderRepository.createDraft(input)
  },

  async 'db:deleteRun'(runId: string) {
    await orderRepository.deleteByRunId(runId)
    return null
  },
})

The repository symbols above are application-specific dependencies, not invented Cypress APIs. Implement them with the service's supported database client, parameterized statements, and least-privileged test credentials.

Include a unique run ID so parallel workers do not overwrite each other's records. Make cleanup scoped and safe to repeat. A failed cleanup should be visible, but it should not delete all shared test data. For large baseline datasets, create them once through environment provisioning rather than before every test.

Never send database passwords through the task argument. Read secrets from the Node process environment, keep them out of Cypress screenshots and Command Logs, and return only identifiers or safe business fields.

8. Inspect Files and Downloads Securely

Node tasks are useful when a downloaded document requires parsing beyond a browser assertion. Constrain paths to a known root so a test argument cannot read arbitrary files from the CI host.

import path from 'node:path'
import { readFile } from 'node:fs/promises'

const downloadsRoot = path.resolve('cypress/downloads')

function safeDownloadPath(name: string): string {
  const candidate = path.resolve(downloadsRoot, name)
  if (!candidate.startsWith(`${downloadsRoot}${path.sep}`)) {
    throw new Error('Download path is outside the allowed directory')
  }
  return candidate
}

on('task', {
  async 'downloads:readUtf8'(name: string) {
    return readFile(safeDownloadPath(name), 'utf8')
  },
})
cy.task<string>('downloads:readUtf8', 'account-summary.csv').then((csv) => {
  expect(csv).to.include('account_id,total')
  expect(csv).to.include('acct-42,125.00')
})

The prefix check includes the platform separator, which prevents a sibling path with a shared text prefix from passing. Decide whether nested directories are allowed, validate extensions, cap file size, and return parsed facts rather than a huge document where possible.

Delete or uniquely name downloads between tests. A task that finds yesterday's artifact can create a convincing false pass. Configure the download folder and cleanup policy explicitly in CI.

File readiness also matters. A browser download may create the path before all bytes are flushed. Synchronize on the browser's completed download behavior when available, or make the inspection task poll metadata with a short stable-size rule and a strict deadline. Never retry parsing forever, and never accept a partially readable document as success.

For large files, read only the required prefix or stream through a parser. Returning a multi-megabyte string across the task boundary increases memory, serialization time, and Command Log noise without improving the assertion.

9. Secure the Node Escape Hatch

A task runs with the permissions of the Cypress Node process. That can include repository files, network access, environment secrets, and database credentials. Treat every handler as privileged test infrastructure.

Avoid handlers such as runAnyCommand, readAnyFile, requestAnyUrl, or executeSql. Instead, expose allowlisted domain operations with narrow typed arguments. Validate lengths, formats, identifiers, hostnames, and paths at runtime, because TypeScript types disappear when the spec executes.

Do not put secrets into task names, arguments that appear in logs, returned objects, screenshots, or assertion messages. The log: false command option can hide the command from the Cypress Command Log, but it is not a complete secret-management strategy. The handler and dependent libraries must also avoid printing credentials.

Consider the failure path. An error from a database client can contain a connection string or SQL values. Catch it in the task module, log a safe correlation identifier when available, and throw a concise sanitized error. Preserve enough context to identify the operation and test run without copying sensitive data.

CI permissions should be least privilege even when tasks are well designed. A checkout test rarely needs production network reach or repository write access outside artifact directories. Defense in depth limits the impact of a mistaken task.

10. Apply Cypress cy.task Reliability Practices

Reliable task design is about bounded work, isolated state, and diagnostic contracts. Every handler should have a clear input schema, result schema, timeout strategy, error shape, and ownership boundary.

Use this production checklist:

  • Return null explicitly when there is no result.
  • Return or await every promise so Cypress sees completion and failure.
  • Add operation deadlines below the Cypress task timeout.
  • Give parallel workers unique data namespaces.
  • Keep credentials in Node-side environment configuration.
  • Use domain tasks instead of unrestricted primitives.
  • Unit test substantial Node logic independently.
  • Record safe operation context and correlation IDs.
  • Clean up resources with scoped, repeatable operations.
  • Move suite-wide provisioning out of individual specs.

Do not start a web server with cy.task(). Start application services before cypress run, confirm readiness in the pipeline, and stop them after the runner exits. A server task does not naturally end, so it conflicts with the command's completion requirement.

The correct cypress cy.task architecture is deliberately small. When handlers multiply, review whether the tests are compensating for missing test APIs or environment tooling. A purpose-built setup service may provide better authorization, observability, and reuse across Cypress, API, and performance suites.

11. Test Task Logic Without Launching Cypress

Keep the handler adapter thin enough that core Node behavior can be tested with the project's unit-test runner. A plain function is faster to exercise and can cover validation, path handling, timeouts, and error sanitization without starting a browser.

// cypress/tasks/runIds.ts
export function assertRunId(value: unknown): asserts value is string {
  if (typeof value !== 'string' || !/^run-[a-z0-9-]{4,60}$/.test(value)) {
    throw new Error('Invalid test run ID')
  }
}

export function createRunTasks(repository: {
  deleteByRunId(runId: string): Promise<number>
}) {
  return {
    async 'db:deleteRun'(runId: unknown) {
      assertRunId(runId)
      const deletedRows = await repository.deleteByRunId(runId)
      return { deletedRows }
    },
  }
}
// cypress/tasks/runIds.test.ts
import { describe, expect, it, vi } from 'vitest'
import { createRunTasks } from './runIds'

describe('db:deleteRun', () => {
  it('rejects an unsafe run ID before calling the repository', async () => {
    const deleteByRunId = vi.fn()
    const tasks = createRunTasks({ deleteByRunId })

    await expect(tasks['db:deleteRun']('../all')).rejects.toThrow(
      'Invalid test run ID',
    )
    expect(deleteByRunId).not.toHaveBeenCalled()
  })
})

Vitest is an illustrative unit runner and must be installed if the project does not already use it. The design does not depend on Vitest. Any Node-capable runner can call the factory and inject a fake repository.

Keep one Cypress smoke test that proves event registration and serialization. Unit tests cannot catch a typo between cy.task('db:deleteRun') and the registered name. Together, the small integration check and focused unit tests cover both wiring and logic without making every validation case pay the browser startup cost.

Test the adapter's result shape as well as its validation. A repository might begin returning a BigInt, Date, Buffer, or driver-specific object that no longer crosses the boundary as expected. Converting that value inside the adapter keeps serialization behavior deliberate and gives the spec a stable plain-data contract.

12. Diagnose Task Failures in CI

When a task fails, classify the failure before changing timeouts. An immediate message that the event was not handled usually points to a name mismatch or missing registration. An undefined result points to a handler branch without return. A timeout means the handler did not settle within the task deadline, while a rejected promise should contain the operation-specific error created by the Node module.

Log a safe run ID, task capability, and dependency correlation ID in the Node process. Do not dump the full argument or error object automatically. Database clients and HTTP libraries may attach credentials, query values, or response bodies.

Confirm that local and CI Node versions, environment variables, working directories, filesystem permissions, and network routes match the task's assumptions. Paths should be resolved from a deliberate project or artifact root rather than the operator's current directory. For intermittent failures, run the plain task function repeatedly with the same sanitized input before rerunning the entire browser suite.

A good task error identifies the failed operation and owned record, such as Seed order failed for run-1042, without exposing the database connection string. That message turns cy.task() from an opaque escape hatch into a diagnosable infrastructure boundary.

13. Govern Task Changes Like Test Infrastructure

Task handlers deserve code review from the owners of the resource they access. A database task affects schemas and cleanup. A filesystem task affects CI artifact boundaries. A process task can execute repository tooling. Reviewers should confirm least privilege, parameter validation, time limits, and safe failure output before focusing on convenience.

Version task contracts through ordinary source control. When a result field changes, update its TypeScript type, handler, unit tests, and consuming specs together. Avoid supporting several ambiguous shapes in one event. A new event name is clearer when old and new behavior must coexist during migration.

Track slow tasks separately in test timing reports. The Cypress command duration identifies the event, while Node-side metrics can identify connection, query, parsing, or process time. Set an owner and service expectation for frequently used setup operations. A task that gradually grows from one second to twenty seconds can dominate every shard before anyone notices the shared cause.

Retire obsolete handlers. An unused task still expands the privileged interface and can retain old credentials, dependencies, or unsafe assumptions. Repository search, typed wrappers, and periodic review make removal straightforward. A small, owned catalog is easier to secure and diagnose than a permanent collection of experimental escape hatches.

Interview Questions and Answers

Q: What problem does cy.task() solve?

It lets a browser-side Cypress spec invoke named code in the Node process. I use it for Node-only capabilities such as controlled database access, filesystem inspection, or a parser library. I do not use it for actions already represented clearly by Cypress browser, HTTP, or file commands.

Q: Where are task handlers registered?

They are registered with on('task', handlers) inside setupNodeEvents for the relevant testing type in Cypress configuration. The property name on the handler object is the event name passed to cy.task().

Q: Why must a task not return undefined?

Cypress fails a task that returns or resolves with undefined. This catches unhandled event names and accidental missing returns. A handler with no result should return null explicitly.

Q: Can cy.task() accept multiple arguments?

It accepts one argument after the event name. I pass a plain object containing the required fields and validate that object in Node. The value must survive JSON serialization.

Q: Are assertions on cy.task() retried?

No, the task result and its chained assertions run once. If the system is eventually consistent, I use explicit bounded polling of an idempotent operation. I avoid rerunning a task that creates or mutates data.

Q: How do you prevent slow tasks from blocking the suite?

I put a deadline on the underlying network, database, or process operation and set a slightly larger task timeout. Heavy global setup belongs in environment provisioning or a pipeline step. I also return only the data the test needs.

Q: What is the biggest security risk with tasks?

Tasks are a privileged Node escape hatch. Generic shell, SQL, URL, or filesystem handlers can expose the CI host and secrets. I provide allowlisted domain operations, validate runtime input, sanitize errors, and run CI with least privilege.

Common Mistakes

  • Forgetting to return null, which leaves the handler returning undefined.
  • Passing several positional values instead of one serializable object.
  • Passing functions, symbols, regular expressions, or circular data across the boundary.
  • Starting a server, watcher, or subscription that never lets the task finish.
  • Setting a huge timeout without a smaller operation-level deadline.
  • Assuming .should() reruns an eventually failing task.
  • Exposing raw SQL, arbitrary commands, URLs, or file paths as task inputs.
  • Sharing mutable Node state across parallel processes as if it were a global database.
  • Performing expensive environment provisioning before every test.
  • Logging credentials in arguments, return values, or dependency errors.

Conclusion

Use cypress cy.task as a narrow bridge to Node, not as a general replacement for Cypress commands. Register named handlers in setupNodeEvents, pass one serializable argument, return plain data or null, and keep every operation bounded. Domain-specific adapters give tests the capability they need without exposing an unrestricted CI machine.

Start by identifying the process boundary. If a direct request, fixture, intercept, or file command already fits, use it. If Node access is essential, build the smallest task contract, validate it at runtime, test its core logic separately, and make its failures safe and specific.

Interview Questions and Answers

Explain the execution boundary of cy.task.

The spec runs in Cypress's browser-related context, while the task handler runs in the Node plugin process. The event name and one argument cross into Node through serialization. The returned or resolved plain value crosses back and becomes the command subject.

What must a task handler return?

It must return a serializable value, a promise that resolves to one, or null when no result is required. It must not return or resolve with undefined. That rule catches missing handlers and accidental fall-through branches.

Are assertions chained to cy.task retried?

No. The task and its chained assertion execute once. For eventual consistency, I implement explicit bounded polling of an idempotent read and never assume should will rerun a mutating task.

How do you organize a large task collection?

I group handlers by capability in Node modules and combine them with namespaced keys such as db:seedOrder and downloads:inspect. Thin adapters validate input and shape output. Core logic remains plain code that unit tests can call directly.

How do you design a secure database task?

I expose a domain action rather than arbitrary SQL, validate every runtime input, and use parameterized queries with least-privileged test credentials. Secrets stay in the Node environment. The result contains only identifiers and safe business fields.

How do you diagnose a cy.task timeout?

I identify which dependency operation did not settle and inspect its own deadline and safe correlation data. I confirm CI network access, working directories, credentials, and permissions before raising timeouts. A task timeout is a symptom, not evidence that the suite simply needs more seconds.

Why should task logic be unit tested separately?

Validation, path checks, sanitization, and dependency errors have many branches that do not need a browser. Plain unit tests give faster and more precise feedback. I keep one Cypress integration check to prove registration, serialization, and the event name.

Frequently Asked Questions

What does cy.task do in Cypress?

cy.task sends a named event and one serializable argument from the spec to Cypress's Node process. The registered handler performs Node-side work and returns a result to the test. Cypress waits for the handler to finish before running later commands.

Where do I register a Cypress task?

Register task handlers with on('task', handlers) inside setupNodeEvents for the relevant testing type in cypress.config.ts or cypress.config.js. The handler property name must match the event passed to cy.task.

Why does cy.task fail when my handler returns undefined?

Cypress treats undefined as evidence that the event was not handled or the handler forgot to return. Return null explicitly when no value is needed. Return or await every promise so Cypress observes completion and errors.

Can cy.task receive multiple arguments?

The command accepts one argument after the event name. Put several values into a plain object and validate them in Node. Functions, symbols, regular expressions, and circular structures do not cross the serialization boundary correctly.

When should I use cy.task instead of cy.request?

Use cy.request for an ordinary application or test-support HTTP endpoint. Use cy.task when the operation needs a Node-only driver, filesystem access, controlled process state, or another Node capability. The narrower built-in command is usually easier to maintain.

How do I change the Cypress task timeout?

Pass a timeout option as the third argument to cy.task or configure taskTimeout deliberately. Give the underlying network, database, or process call a smaller internal deadline. A larger Cypress timeout alone makes failures slower and less specific.

Can cy.task start a web server?

It should not. A task must eventually finish, while a server is designed to remain running. Start services before Cypress in the development or CI workflow, verify readiness, and stop them after the test process exits.

Related Guides