# How to Debug Cordis Fiber and Effect Issues: A Complete Guide

> Debug Cordis fiber and effect issues effectively. Learn to inspect state, enable logging, and verify effect disposal for robust application development.

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

---

**To debug Cordis fiber and effect issues, inspect the `Fiber.state` property and `Fiber._error` field to identify lifecycle failures, enable detailed logging via `enableLogs: true`, and use `fiber.getEffects()` to verify proper disposal of resources created through `ctx.fiber.effect`.**

Debugging Cordis fiber and effect issues requires understanding the framework's execution model, where **fibers** manage plugin lifecycles and **effects** handle disposable resources. In the `cordiverse/cordis` repository, the `Fiber` class in [`/packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main//packages/core/src/fiber.ts) orchestrates state transitions from `PENDING` to `DISPOSED`, while the effect system tracks resource cleanup through disposable lists. This guide walks through the source code architecture, common failure modes, and practical debugging techniques to resolve stuck fibers, leaking effects, and unexpected state transitions.

## Understanding the Cordis Fiber Architecture

### The Fiber State Machine

Every plugin in Cordis runs inside a **Fiber** instance created by `new Fiber(parent, config, inject, runtime, getOuterStack)` in [`/packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main//packages/core/src/registry.ts)【/packages/core/src/registry.ts#L93-L108】. The fiber implements a strict state machine via `Fiber.state`, which transitions through `FiberState` values: `PENDING` → `LOADING` → `ACTIVE` → `UNLOADING` → `DISPOSED`【/packages/core/src/fiber.ts#L78-L85】.

Key lifecycle hooks control these transitions:

- **`_refresh`** – Runs after fiber creation or when injected dependencies change. It recomputes the epoch string and may trigger reload or unload operations【/packages/core/src/fiber.ts#L85-L97】.
- **`_setEpoch`** – Called when the epoch changes; decides whether to transition to `LOADING` for a reload or `UNLOADING` for disposal【/packages/core/src/fiber.ts#L99-L112】.
- **`_reload`** – Executes the plugin's `execute` function and activates the fiber【/packages/core/src/fiber.ts#L15-L35】.
- **`_unload`** – Disposes all registered effects and transitions to `DISPOSED`【/packages/core/src/fiber.ts#L36-L48】.

When disposal occurs, `Fiber.dispose` removes the fiber from `runtime.fibers`, clears its configuration, and waits for pending `inertia` promises to settle【/packages/core/src/fiber.ts#L70-L99】.

### Effect Lifecycle and Tracking

Effects are created via `ctx.fiber.effect(() => …, 'label')`, implemented in `Fiber.effect`【/packages/core/src/fiber.ts#L75-L84】. The executor function may return:

- A **synchronous disposable** (`() => void`)
- An **iterable of disposables**
- A **promise of a disposable**
- An **async iterable**

The fiber stores each disposable in `Fiber._disposables` (a `DisposableList`) and builds a tree of `EffectMeta` objects for debugging purposes【/packages/core/src/fiber.ts#L22-L31】【/packages/core/src/fiber.ts#L92-L106】.

## Common Cordis Fiber and Effect Failure Modes

**Fiber stays in `PENDING` state** – This occurs when `Fiber._runner.epoch` never leaves `INACTIVE`, typically due to validation errors during `resolveConfig` or exceptions thrown before `_refresh` runs. Check `Fiber._error` (exposed via `await()` which re-throws) or `ctx.logger.error` output inside `Fiber.dispose`【/packages/core/src/fiber.ts#L73-L78】.

**Effect never runs or never disposes** – The effect executor likely returned a non-function or non-iterable value, triggering `TypeError('Invalid effect')`. The error is wrapped by `composeError` in [`/packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main//packages/core/src/utils.ts), so inspect the stack trace printed by `ctx.logger.error`【/packages/core/src/utils.ts#L60-L78】.

**`ctx.fiber.dispose()` is ignored** – This happens when the fiber's `uid` is already `null` (previously disposed) or the fiber is mid-reload/unload, causing an early return. Verify `fiber.uid` and `fiber.state` via console logging before calling dispose.

**Unexpected `FiberState.FAILED`** – An exception bubbled out of `_reload` or `_unload`. The error is stored in `Fiber._error` and re-thrown by `await()`【/packages/core/src/fiber.ts#L60-L66】. Call `await fiber` in a test or REPL to surface the underlying error.

## Step-by-Step Debugging Workflow

Follow this systematic approach when debugging Cordis fiber and effect issues:

1. **Enable detailed logging** – Set `ctx.fiber.entry?.parent.tree.enableLogs = true` or pass `enableLogs: true` in the loader configuration. The `LoggerService` prints timestamps and stack traces for every `ctx.logger.error` call.

2. **Inspect the fiber's metadata** – Access the internal state to diagnose lifecycle issues:

   ```typescript
   const fiber = ctx.fiber // root fiber
   console.log('state →', fiber.state)
   console.log('epoch →', (fiber as any)._runner?.epoch)
   console.log('effects →', fiber.getEffects().map(m => m.label))
   ```

3. **Force a reload** – If you suspect stale configuration, invoke `fiber.restart()` or `fiber.update(newConfig)` to reset the epoch and trigger `_refresh`/`_reload`【/packages/core/src/fiber.ts#L68-L74】.

4. **Break on errors** – Insert a temporary `debugger` statement inside `Fiber._execute` or `composeError` to capture the exact call stack when an effect throws.

5. **Unit-test the problematic plugin** – Use the test harness in `packages/core/tests/*.spec.ts` to isolate the plugin. The spec files create a fresh context and expose the fiber via `ctx.plugin(Loader)`; you can then call `await fiber` and assert on `fiber.state` or `fiber._error`.

## Practical Debugging Examples

### Example 1: Checking a Fiber's State in a Plugin

Use this pattern to verify your plugin reaches the `ACTIVE` state and to inspect effect registration:

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

export default class Demo {
  @Inject('logger')
  apply(ctx: Context) {
    const fiber = ctx.fiber
    // Log the current state
    ctx.logger.info('Fiber state:', fiber.state)
    
    // Create an effect that cleans up a timer
    return ctx.fiber.effect(() => {
      const timer = setInterval(() => ctx.logger.debug('tick'), 1000)
      return () => clearInterval(timer) // disposable
    }, 'demo-timer')
  }
}

```

When the plugin loads successfully, you will see "Fiber state: ACTIVE" in the console. If the plugin throws during `apply`, the state becomes `FAILED` and the error prints automatically.

### Example 2: Triggering a Reload After Config Change

Test how your plugin handles configuration updates by forcing a reload cycle:

```typescript
// Assume `ctx` is a running Cordis context
const fiber = await ctx.plugin(Demo, { interval: 500 })
await fiber // Wait for initial load

// Update config to force reload
fiber.update({ interval: 200 })
await fiber // Wait for reload to finish
ctx.logger.info('Reload complete, state:', fiber.state)

```

If the new config fails validation and `resolveConfig` throws a `ValidationError`, `fiber.state` becomes `FAILED` and the error is logged via the fiber's error handling mechanism.

### Example 3: Debugging a Hanging Effect

Identify async iterables that never complete using this test pattern:

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

describe('hanging effect', () => {
  it('should not leave dangling disposables', async () => {
    const ctx = new Context()
    const fiber = await ctx.plugin({ 
      name: 'leaky', 
      apply: (c) => {
        // Returns an async iterable that never finishes
        return c.fiber.effect(async function* () {
          while (true) yield () => {}
        }, 'leaky')
      }
    })
    
    // Wait a tick then dispose
    await new Promise(r => setTimeout(r, 10))
    await fiber.dispose()
    
    // All disposables must be cleared
    expect(fiber.getEffects()).to.be.empty
  })
})

```

If the async iterable never yields a final `done` value, the test surfaces the problem by checking `fiber.getEffects()` after disposal, which should be empty in a properly cleaned-up fiber.

## Key Source Files for Debugging Cordis

Understanding these core files accelerates root cause analysis:

- **[`/packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main//packages/core/src/fiber.ts)** – Implements the `Fiber` class, state machine transitions, effect handling, and reload/unload logic. Contains `Fiber.effect`, `Fiber.dispose`, and the `_refresh`/`_setEpoch` hooks.

- **[`/packages/core/src/context.ts`](https://github.com/cordiverse/cordis/blob/main//packages/core/src/context.ts)** – Sets up the root `Context`, creates the initial fiber, and exposes the `ctx.fiber` property for accessing the current execution context.

- **[`/packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main//packages/core/src/registry.ts)** – Handles plugin registration via `RegistryService.plugin`, creates a `Fiber` for each plugin, and provides the `ctx.plugin` and `ctx.inject` APIs.

- **[`/packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main//packages/core/src/utils.ts)** – Supplies utility helpers including `DisposableList`, `composeError`, and `buildOuterStack` used throughout the fiber and effect system.

## Summary

- **Cordis fibers** manage plugin lifecycles through a strict state machine (`PENDING` → `LOADING` → `ACTIVE` → `UNLOADING` → `DISPOSED`) implemented in [`/packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main//packages/core/src/fiber.ts).
- **Effects** must return valid disposables (functions, iterables, or promises); invalid returns trigger `TypeError` wrapped by `composeError` in [`/packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main//packages/core/src/utils.ts).
- **Debug stuck fibers** by checking `fiber.state`, `fiber._error`, and `(fiber as any)._runner?.epoch` to identify validation or dependency resolution failures.
- **Force reloads** using `fiber.update()` or `fiber.restart()` to test configuration changes and verify `_refresh` behavior.
- **Verify cleanup** by asserting `fiber.getEffects()` is empty after disposal to catch hanging async iterables or undisposable resources.

## Frequently Asked Questions

### Why does my Cordis fiber stay stuck in the PENDING state?

A fiber remains `PENDING` when `Fiber._runner.epoch` never transitions from `INACTIVE`, usually due to validation errors during `resolveConfig` or an exception thrown before the `_refresh` hook executes. Inspect `Fiber._error` (re-thrown by `await fiber`) or check the logger output from `Fiber.dispose` to identify the blocking error【/packages/core/src/fiber.ts#L73-L78】.

### How do I detect if a Cordis effect failed to dispose properly?

Call `fiber.getEffects()` after disposal; if the array is not empty, resources remain active. This commonly occurs when an effect executor returns an async iterable that never completes or a non-disposable value. The error is captured by `composeError` and logged via `ctx.logger.error` with a stack trace built by `buildOuterStack`【/packages/core/src/utils.ts#L60-L78】.

### What causes a Cordis fiber to enter the FAILED state unexpectedly?

The `FAILED` state occurs when an exception bubbles out of `_reload` or `_unload` during the plugin lifecycle. The error is stored in `Fiber._error` and will be re-thrown when you `await` the fiber. Check the stack trace to distinguish between errors in the plugin's `apply` function and errors in the disposal logic【/packages/core/src/fiber.ts#L60-L66】.

### How can I force a Cordis plugin to reload for debugging purposes?

Invoke `fiber.update(newConfig)` to change the configuration and trigger a reload, or call `fiber.restart()` to reset the epoch and re-execute the plugin. Both methods trigger `_setEpoch` and `_refresh`, allowing you to test how the plugin handles state transitions and configuration validation【/packages/core/src/fiber.ts#L68-L74】.