# How Cordis Implements Effect Tracking and Automatic Disposal

> Cordis uses a fiber-based architecture for effect tracking and automatic disposal. Learn how Cordis manages resources efficiently during plugin unload, hot-reload, or explicit disposal.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: internals
- Published: 2026-09-12

---

**Cordis implements effect tracking through a fiber-based architecture that registers disposables in a hierarchical tree, automatically cleaning up resources when plugins unload, hot-reload, or explicitly dispose.**

Cordis is a progressive TypeScript framework for building modular applications, and its effect tracking and automatic disposal system ensures that plugins can register cleanup logic that executes reliably when contexts are destroyed. The implementation centers on the `Fiber` class in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts), which manages the lifecycle of plugin code through a structured disposable pattern.

## The Fiber Architecture and Effect Creation

Each plugin in Cordis operates within a `Context` that exposes an `effect` method. This method originates from `Fiber.effect` (lines 75‑78 in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)), which first validates that the current fiber is active using `assertActive` before proceeding. This validation ensures that disposal operations cannot be initiated on already-terminated fibers.

When a plugin calls `ctx.effect()`, the framework creates an **EffectMeta** object that tracks parent-child relationships between nested effects using the `symbols.effect` symbol. This metadata structure forms a tree that enables cascading disposal—when a parent effect is disposed, all its children are automatically cleaned up.

## Collecting and Registering Disposables

### The DisposableList and EffectMeta Tree

Inside the effect runner, the `collect` function (lines 99‑106 in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)) pushes each returned disposable into `this._disposables`, which is an instance of `DisposableList` defined in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts). Simultaneously, the system links the disposable into the EffectMeta tree via `meta.children.push(dispose[symbols.effect])`, establishing the parent-child hierarchy that enables automatic propagation of disposal operations.

### Handling Different Disposable Types

The `_execute` method (lines 29‑73 in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)) handles various return types from effect functions, including synchronous functions, promises, iterables, and async iterables. This flexibility allows developers to return simple cleanup functions, arrays of disposables, or async teardown logic, all of which are normalized and registered for automatic cleanup.

## Automatic Disposal Lifecycle

### The _unload Method

When disposal is triggered, the fiber invokes `_unload` (lines 39‑50 in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)). This method iterates over `_disposables.clear()`, executing each registered disposable sequentially. The implementation uses `composeError` to catch and log any errors during cleanup, swallowing exceptions to prevent unhandled rejections while ensuring all disposables run to completion.

### Disposal Triggers

Cordis triggers automatic disposal through several mechanisms:

- **Plugin Self-Dispose**: When a plugin calls `dispose()`, it invokes `ctx.fiber.dispose()`, starting the `_unload` sequence.
- **Hot-Module Replacement (HMR)**: During reloads, the HMR system changes the fiber's epoch via `_setEpoch`, which initiates `_unload` for the old fiber before loading the new code (implemented in [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts)).
- **Manual Restart**: The `Fiber.restart` method forces a new epoch marked as `INACTIVE`, then runs `_reload`, which ultimately calls `_unload` for the previous epoch.

## Practical Implementation Examples

The following examples demonstrate how to use `ctx.effect()` to register disposables that clean up automatically when the plugin unloads:

```typescript
// Example 1 – Simple effect with manual cleanup
export function myPlugin(ctx: Context) {
  // Register a timeout that will be cleared automatically
  const disposeTimer = ctx.effect(() => {
    const id = setTimeout(() => console.log('tick'), 1000)
    // Return a disposable that clears the timer
    return () => clearTimeout(id)
  }, 'myTimer')
}

```

```typescript
// Example 2 – Effect that returns an async disposable
export function anotherPlugin(ctx: Context) {
  ctx.effect(async () => {
    const conn = await db.connect()
    // Return an async disposable that closes the connection
    return async () => await conn.close()
  }, 'dbConnection')
}

```

```typescript
// Example 3 – Nested effects – child disposables are auto‑disposed
export function nestedPlugin(ctx: Context) {
  ctx.effect(() => {
    // Child effect
    ctx.effect(() => {
      const subId = setInterval(() => console.log('sub'), 500)
      return () => clearInterval(subId)
    }, 'subTimer')
    // Parent disposable
    const id = setTimeout(() => console.log('parent'), 2000)
    return () => clearTimeout(id)
  }, 'parentTimer')
}

```

When any of these plugins are unloaded via HMR, `ctx.fiber.dispose()`, or manual restart, all timers and database connections are automatically cleared because they were registered through `ctx.effect`.

## Summary

- Cordis uses a **fiber-based architecture** where `Fiber.effect` (in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)) creates disposable effects that validate the active state before registration.
- The system maintains a **hierarchical tree of EffectMeta objects** (using `symbols.effect`) to track parent-child relationships between nested effects, ensuring children dispose when parents dispose.
- Disposables are stored in a **DisposableList** and executed sequentially by `_unload` (lines 39‑50), which catches and logs errors via `composeError` without halting cleanup.
- **Automatic disposal** triggers through plugin unload events, HMR epoch changes, or manual `Fiber.restart`, guaranteeing resource cleanup across hot-reloads and module replacements.
- The `_execute` method handles synchronous functions, promises, iterables, and async iterables, providing flexibility for various cleanup patterns including database connections and timers.

## Frequently Asked Questions

### What happens if an effect throws an error during disposal?

The `_unload` method (lines 39‑50 in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)) wraps each disposable execution in error handling using `composeError`. If an error occurs during cleanup, the framework logs the error and continues executing the remaining disposables, ensuring that one failed cleanup does not prevent others from running.

### Can effects be nested in Cordis?

Yes, Cordis supports nested effects through the **EffectMeta tree structure**. When you call `ctx.effect()` inside another effect, the system registers the child effect using `meta.children.push(dispose[symbols.effect])` (lines 103‑105). This establishes a parent-child relationship where disposing the parent automatically disposes all nested children.

### How does HMR trigger automatic disposal in Cordis?

During hot-module replacement, the HMR system (in [`packages/hmr/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/hmr/src/index.ts)) changes the fiber's epoch via `_setEpoch`. This epoch change signals that the old fiber is inactive, triggering `_unload` to clear all registered disposables before the new module code loads, preventing resource leaks between reloads.

### What types of values can be returned from an effect function?

The `_execute` method (lines 29‑73 in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)) accepts multiple return types: synchronous functions, promises that resolve to disposables, iterables of disposables, and async iterables. This allows you to return simple cleanup functions, arrays of cleanup tasks, or async teardown operations, all normalized for automatic cleanup.