# Cordis Effect System Automatic Cleanup: How Plugins Self-Dispose

> Discover how the Cordis effect system automatically cleans up plugin resources using disposable effects and reverse-order cleanup functions. Learn about efficient resource management.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: how-to-guide
- Published: 2026-08-25

---

**Cordis implements a robust effect system that automatically disposes of resources when a plugin is unloaded by registering lifecycle hooks as disposable effects that execute cleanup functions in reverse order when the parent fiber enters a DISPOSED state.**

The Cordis framework (`cordiverse/cordis`) provides a sophisticated plugin architecture where resource management is handled automatically through its effect system. When a plugin initializes, every resource it creates—timers, database connections, or event listeners—registers as an effect that cleans itself up when the plugin unloads. Understanding how Cordis effect system automatic cleanup works at the source code level is essential for building reliable, leak-free plugins.

## Core Effect Registration in the Fiber Constructor

Every plugin creates a **root effect** that wraps its entire lifecycle. In [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts), the `Fiber` constructor registers a disposal effect via `parent.fiber.effect(...)` (lines 70-100). This registration associates the plugin's lifecycle with the parent fiber's cleanup chain.

The disposal effect returns an async cleanup function that executes when the plugin's fiber is disposed. According to the source code, this function:

- Emits the `'internal/plugin'` event to signal teardown
- Removes the plugin from the internal registry
- Resets the epoch to `INACTIVE`
- Awaits any pending inertia work before completing

This ensures that when a plugin is unloaded, the framework systematically tears down the plugin's resources before finalizing the disposal.

## The Fiber.effect Method and Active State Validation

The `Fiber.effect` method serves as the public entry point for creating effects in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) (lines 75-106). Before creating any effect, the system validates that the current fiber is active by calling `assertActive`. This validation prevents resource leaks by ensuring effects cannot be created on already-disposed fibers.

Once validated, the method builds a **disposable wrapper** that intercepts the user's effect function and manages its lifecycle. This wrapper is what enables the automatic cleanup semantics that Cordis provides.

## Automatic Disposal Chain and Execution Order

Inside the `effect` method, the wrapper collects all disposables returned by the user's effect function. When the wrapper itself is invoked—or when the parent fiber is disposed—the disposal chain executes with specific guarantees defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) (lines 22-30):

1. **Reverse order execution** – Disposables run in LIFO (last-in-first-out) order, ensuring that resources created last are cleaned up first
2. **Promise chaining** – Any returned promises are properly chained to ensure asynchronous cleanup operations complete sequentially
3. **Epoch flag clearing** – The internal `runner.epoch` flag is cleared to prevent double-execution of cleanup code

This mechanism ensures that nested resources are disposed of safely, with inner resources always cleaned up before outer resources that might depend on them.

## Effect Metadata and State Management

Each disposable in the Cordis effect system is tagged with `symbols.effect` metadata, defined in the utility files. This metadata enables introspection capabilities through the `getEffects()` method (lines 42-46 in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)), which enumerates the current effect tree.

The fiber tracks its lifecycle through the `FiberState` enumeration. When `dispose` runs, the state transitions to `DISPOSED`, causing any remaining effects to be marked inactive. This state change prevents new effect creation; attempts to call `ctx.effect` on an inactive fiber throw `CordisError.INACTIVE_EFFECT`.

## Practical Implementation Examples

The following examples demonstrate how to leverage the automatic cleanup mechanism in Cordis plugins.

### Registering a Simple Timer Effect

This example shows how to register a cleanup function for a `setInterval` timer that automatically clears when the plugin unloads:

```typescript
import { Context } from 'cordis'

export default function (ctx: Context) {
  // This effect runs when the plugin is disposed
  ctx.effect(() => {
    const timer = setInterval(() => console.log('tick'), 1000)
    // Return a cleanup function – Cordis will call it automatically
    return () => clearInterval(timer)
  }, 'my-plugin:timer')
}

```

### Async Effects with Database Connections

For asynchronous resources, return an async cleanup function. Cordis will await the cleanup during the disposal phase:

```typescript
import { Context } from 'cordis'

export default async function (ctx: Context) {
  ctx.effect(async () => {
    const conn = await createDatabaseConnection()
    // When the plugin is unloaded, Cordis will await this cleanup
    return async () => await conn.close()
  }, 'db-connection')
}

```

### Inspecting the Effect Tree for Debugging

You can enumerate active effects using the `getEffects()` method to monitor resource usage:

```typescript
import { Context } from 'cordis'

export default function (ctx: Context) {
  ctx.effect(() => {
    // ... some work
  }, 'debug:example')
  // Retrieve meta-information about all active effects
  console.log(ctx.fiber.getEffects())
}

```

## Summary

- **Automatic disposal** is guaranteed for all resources registered via `ctx.effect()` when the owning plugin unloads, eliminating manual teardown logic
- **Reverse execution order** ensures that dependent resources are cleaned up safely, with inner disposables executing before outer ones
- **Async safety** is built-in through promise chaining, allowing database connections and other async resources to close properly before the fiber terminates
- **State protection** via `FiberState` and `CordisError.INACTIVE_EFFECT` prevents resource leaks by blocking new effects on disposed fibers
- **Metadata introspection** through `symbols.effect` and `getEffects()` enables debugging and monitoring of the active effect tree in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)

## Frequently Asked Questions

### How does Cordis handle asynchronous cleanup functions?

Cordis automatically detects when an effect's cleanup function returns a Promise. According to the implementation in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts), the disposal chain chains these promises sequentially, ensuring that async resources like database connections or file handles close completely before the fiber state transitions to `DISPOSED`. This prevents race conditions where a plugin might terminate while cleanup operations are still pending.

### Can I inspect which effects are currently active in my plugin?

Yes. The `getEffects()` method in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) (lines 42-46) returns metadata about all active effects tagged with `symbols.effect`. You can call `ctx.fiber.getEffects()` to enumerate the current effect tree, which is useful for debugging memory leaks or verifying that resources are properly registered. This introspection capability is used internally by the framework to report status and trigger cascading clean-ups.

### What happens if I try to create an effect after a plugin starts unloading?

The system throws `CordisError.INACTIVE_EFFECT`. When a fiber's state transitions to `DISPOSED` during the unload cycle, the `assertActive` check inside `Fiber.effect` fails, preventing new resource registration. This safety mechanism ensures that developers cannot accidentally create dangling resources during plugin teardown, as the source code explicitly validates fiber state before allowing effect creation.

### Where is the Context interface defined if I want to type my plugin correctly?

The `Context` interface, which includes the `effect` method signature, is declared in [`packages/core/src/context.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts). This file defines the public API surface for creating effects, while the core implementation logic resides in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts). For utility types and symbols like `symbols.effect`, reference [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts).