# How Cypress Implements the Cypress.on() and cy.on() Event System

> Discover how Cypress implements its event system using EventEmitter2 and the $Cy class. Learn how Cypress.on() and cy.on() provide a unified event API for seamless testing.

- Repository: [Cypress.io/cypress](https://github.com/cypress-io/cypress)
- Tags: internals
- Published: 2026-07-12

---

**Cypress builds its event system on top of EventEmitter2, using a `$Cy` class that extends EventEmitter2 to power the `cy` object, while an `extend` helper proxies EventEmitter2 methods onto the global `Cypress` object to create a unified event API.**

The `cypress-io/cypress` repository implements a sophisticated event architecture that powers both the global `Cypress.on()` API and the per-test `cy.on()` method. This system allows developers to hook into test lifecycle events, command execution, and browser interactions through a consistent interface. Understanding how Cypress implements this event system reveals why the global and instance-level APIs behave identically while maintaining separate event scopes.

## Core Architecture: EventEmitter2 Foundation

Cypress does not implement its own event system from scratch. Instead, it relies on the **EventEmitter2** library to provide the underlying listener management and emission capabilities.

The architecture consists of two cooperating pieces:

- **`$Cy` class** – A subclass of `EventEmitter2` defined in [`packages/driver/src/cypress/cy.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cypress/cy.ts) that powers the `cy` object. It holds test-run state, the command queue, and registers listeners for Cypress-wide events.
- **`extend` helper** – A utility in [`packages/driver/src/cypress/events.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cypress/events.ts) that copies the EventEmitter2 API onto any object, specifically the global `Cypress` object.

## The $Cy Class Implementation

The `$Cy` class serves as the concrete implementation of the `cy` object that test code interacts with. Because it inherits from `EventEmitter2`, it natively possesses methods like `.on()`, `.once()`, `.off()`, and `.emit()`.

When a test run starts, the driver creates a new `$Cy` instance (referenced as `curCy` in lines 72-74 of [`cy.ts`](https://github.com/cypress-io/cypress/blob/main/cy.ts)):

```typescript
// packages/driver/src/cypress/cy.ts
const cy = new $Cy()

```

This instance holds the current test's state and becomes the target for event proxying from the global `Cypress` object.

## The extend Helper and Global Cypress Object

The global `Cypress` object that users access in support files and plugins receives its event capabilities through the `extend` function in [`events.ts`](https://github.com/cypress-io/cypress/blob/main/events.ts). This function creates a fresh `EventEmitter2` instance and proxies its methods onto the passed-in object:

```typescript
// packages/driver/src/cypress/events.ts
import { extend as extendEvents } from './events'

const Cypress = {} as any
extendEvents(Cypress) // Adds .on, .once, .emit, emitThen, etc.

```

The `extend` function also wires a `proxyTo` method that allows a child emitter (the `$Cy` instance) to forward its events to the parent while managing logging behavior.

## Event Proxying and Forwarding

The connection between `Cypress.on()` and `cy.on()` happens through explicit proxying. Inside [`cy.ts`](https://github.com/cypress-io/cypress/blob/main/cy.ts), the driver hooks the `$Cy` instance into the global emitter:

```typescript
// packages/driver/src/cypress/cy.ts
const cy = new $Cy()
extendEvents(Cypress).proxyTo(cy)

```

From this point forward, any call to `Cypress.on('test:before:run', fn)` registers the listener on the underlying `$Cy` emitter. The `proxyTo` method (lines 30-68 in [`events.ts`](https://github.com/cypress-io/cypress/blob/main/events.ts)) forwards events to the parent while temporarily silencing duplicate logs, preventing double-printing when both `Cypress` and `cy` listen to the same event.

## Lifecycle Events and Emission

Throughout the driver codebase, events are emitted at critical lifecycle points:

- **`Cypress.emit('enqueue:command')`** – Emitted in [`cy.ts`](https://github.com/cypress-io/cypress/blob/main/cy.ts) when a command is queued (line 353)
- **`Cypress.emit('test:before:run')`** – Emitted during test lifecycle transitions in [`command_queue.ts`](https://github.com/cypress-io/cypress/blob/main/command_queue.ts) and [`proxy-logging.ts`](https://github.com/cypress-io/cypress/blob/main/proxy-logging.ts)
- **`Cypress.emit('command:enqueued')`** – Used to notify listeners when commands enter the queue

EventEmitter2 provides rich emission patterns including `emit`, `emitThen`, and `emitThenSeries`, which Cypress wraps with logging controls (`logEmit` flag) to conditionally output debug information.

## Practical Usage Examples

### Global Event Listeners

Use `Cypress.on()` in support files or plugins to handle events across all tests:

```typescript
// cypress/support/e2e.ts
Cypress.on('uncaught:exception', (err) => {
  // Return false to prevent Cypress from failing the test
  return !err.message.includes('ResizeObserver loop')
})

```

### Per-Test Listeners

Use `cy.on()` within individual tests for scoped event handling:

```typescript
it('tracks command queue', () => {
  cy.on('command:enqueued', (cmd) => {
    console.log('Queued command:', cmd)
  })

  cy.get('button').click()
})

```

### Custom Events

Emit and listen to custom events for plugin communication:

```typescript
// Listening
Cypress.on('my:plugin:event', (payload) => {
  console.log('Plugin says:', payload)
})

// Emitting (rare in test code, but possible)
Cypress.emit('my:plugin:event', { foo: 'bar' })

```

## Key Source Files

Understanding the implementation requires examining these specific files:

- **[`packages/driver/src/cypress/cy.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cypress/cy.ts)** – Defines the `$Cy` class and wires it to the global `Cypress` emitter
- **[`packages/driver/src/cypress/events.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cypress/events.ts)** – Implements `extend()` which mixes EventEmitter2 methods onto any object
- **[`packages/driver/src/cypress/command_queue.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cypress/command_queue.ts)** – Shows usage of `Cypress.once('command:enqueued')` for command lifecycle management
- **[`packages/driver/src/cypress/proxy-logging.ts`](https://github.com/cypress-io/cypress/blob/main/packages/driver/src/cypress/proxy-logging.ts)** – Emits lifecycle events such as `request:event` and `test:before:run`

## Summary

- **EventEmitter2 Foundation** – Cypress leverages EventEmitter2 rather than building a custom event system, inheriting methods like `on`, `once`, `emit`, and `emitThen`.
- **Dual API Design** – The `$Cy` class provides the `cy.on()` API, while the `extend` helper in [`events.ts`](https://github.com/cypress-io/cypress/blob/main/events.ts) proxies these capabilities to the global `Cypress.on()` API.
- **Proxy Pattern** – The `proxyTo` method connects global and instance-level event emitters, ensuring events flow correctly while preventing duplicate logging.
- **Lifecycle Integration** – The driver emits standard events like `test:before:run`, `command:enqueued`, and `uncaught:exception` throughout the test lifecycle.

## Frequently Asked Questions

### What is the difference between Cypress.on() and cy.on()?

**`Cypress.on()` registers listeners on the global event emitter**, affecting all tests in the run, while **`cy.on()` registers listeners on the specific test's `$Cy` instance**. However, because the global `Cypress` object proxies events to the current `$Cy` instance through the `proxyTo` method, both APIs ultimately operate on the same underlying event bus during test execution.

### Why does Cypress use EventEmitter2 instead of Node.js EventEmitter?

**EventEmitter2 provides advanced features like namespaced events, wildcards, and asynchronous emission patterns** (`emitThen`, `emitThenSeries`) that the standard Node.js EventEmitter lacks. These capabilities are essential for Cypress's complex event handling across test lifecycles, command queues, and browser automation.

### How do I prevent duplicate event handlers when using both Cypress.on() and cy.on()?

**The `proxyTo` implementation in [`events.ts`](https://github.com/cypress-io/cypress/blob/main/events.ts) (lines 30-68) automatically suppresses duplicate logging**, but you should still use `Cypress.once()` or manually call `.off()` if you need to ensure a handler fires only once. For per-test cleanup, `cy.on()` listeners automatically scope to the test and do not persist across tests, while `Cypress.on()` listeners persist until explicitly removed.

### Can I emit custom events in Cypress?

**Yes, you can emit custom events using `Cypress.emit('event:name', payload)`**, though this is rare in standard test code. This pattern is more commonly used in plugins and internal driver code to communicate between different parts of the Cypress architecture, such as between the driver and the reporter.