# How Fiber Effects Handle Asynchronous Resource Management in Cordis

> Cordis's Fiber class enables asynchronous resource management. Discover how it handles Promises, AsyncIterables, and disposables for seamless acquisition and cleanup.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: deep-dive
- Published: 2026-08-24

---

**Yes, Cordis's Fiber class treats effects as first-class primitives that fully support asynchronous resource acquisition and cleanup, handling Promises, AsyncIterables, and synchronous disposables through a unified disposal pipeline.**

Cordis is a modern plugin framework built around Fiber-based lifecycle management. The `Fiber` component in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) provides a robust mechanism for handling resources that require asynchronous initialization or teardown, such as database connections, network servers, or external processes. Understanding how **Fiber effects handle asynchronous resource management** enables developers to build reliable plugins with guaranteed cleanup semantics.

## The Effect Type System: Supporting Async Primitives

Cordis defines a flexible type hierarchy that accommodates both synchronous and asynchronous resource patterns.

### Union Types for Sync and Async Effects

In [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) (lines 54-65), the `Effect` type is defined as a discriminated union:

```ts
type Effect<T = any> =
  | SyncEffect<T>          // () => Disposable<T> | Iterable<Disposable<T>>
  | AsyncEffect<T>         // () => Promise<Disposable<T>> | AsyncIterable<Disposable<T>>

```

An `AsyncEffect` can return a `Promise<Disposable<T>>` for single async resources or an `AsyncIterable<Disposable<T>>` for streaming multiple resources over time. This design allows a single `Fiber.effect` call to handle both immediate async acquisition and batched resource creation.

## Execution Flow for Asynchronous Effects

When `Fiber.effect` is invoked, the framework routes the operation through the `_execute` method to handle resolution and collection of disposables.

### Promise Resolution in _execute

The `_execute` implementation (lines 29-72) detects thenable objects by checking for a `then` property. If the effect returns a Promise, the fiber awaits resolution and stores the resulting disposable. Specific handling for promise-based effects appears at lines 90-95, where the resolved value is pushed onto the internal `_disposables` stack.

### AsyncIterable Handling

For effects returning async generators, the execution path at lines 56-68 iterates using `Symbol.asyncIterator`. Each yielded disposable is collected asynchronously, allowing resources to be constructed and registered incrementally. This enables patterns like connection pooling where each worker must be initialized and cleaned up independently.

## Guaranteed Cleanup with DisposableList

Asynchronous cleanup requires careful sequencing to prevent resource leaks during hot-reloads or shutdowns.

### The _unload Sequence

The `_unload` method (lines 37-48) iterates through the `DisposableList` in reverse registration order (LIFO), awaiting any promise returned by a disposer before proceeding to the next. This sequential awaiting ensures that dependent resources teardown in the correct order. The disposables stack is maintained at lines 12-14, providing O(1) push and pop operations for resource tracking.

## State Management and Error Propagation

Cordis tracks asynchronous operation state to prevent race conditions during load and unload cycles.

### Tracking Async Operations with Inertia

The `_setEpoch` method (lines 99-115) manages the `inertia` promise, which represents the current load or unload operation. During async effect execution, the fiber enters `FiberState.LOADING` or `FiberState.UNLOADING` states. Developers can await `fiber.await()` to guarantee that all async effects have settled before proceeding, ensuring deterministic state transitions.

### Error Handling in Async Flows

If an async effect throws or a disposable rejects, the error is wrapped via `composeError` and propagated to `ctx.logger.error` (lines 21-28). The fiber transitions to `FiberState.FAILED`, and subsequent calls to `await` re-throw the error, allowing explicit error handling in plugin code while preventing unhandled promise rejections.

## Practical Code Examples

### Async Database Connection

Acquire a database client asynchronously and ensure connection closure during teardown:

```typescript
export default function (ctx: Context) {
  ctx.effect(async () => {
    const client = await createDbClient();   // async acquisition
    return async () => {
      await client.close();                  // async clean-up
    };
  }, 'db-client');
}

```

The effect returns a `Promise<Disposable>`; `Fiber.effect` stores the resolved disposer and executes it during the unload cycle (see lines 90-95).

### Streaming Disposables with Async Generators

Manage a pool of workers where each initialization is asynchronous:

```typescript
export default function (ctx: Context) {
  ctx.effect(async function* () {
    for (let i = 0; i < 3; i++) {
      const w = await spawnWorker(i);
      yield async () => {
        await w.terminate();                 // async cleanup per worker
      };
    }
  }, 'worker-pool');
}

```

Because the effect returns an `AsyncIterable<Disposable>`, the fiber iterates at lines 56-68, collecting each disposer and awaiting them sequentially on unload.

### Coordinating Mixed Sync and Async Resources

Combine different resource types while maintaining disposal order:

```typescript
export default function (ctx: Context) {
  ctx.effect(() => {
    const server = http.createServer(app);
    server.listen(0);
    return async () => {
      await new Promise<void>((resolve) => server.close(() => resolve()));
    };
  }, 'http-server');

  ctx.effect(async () => {
    const cache = await initCache();         // async init
    return () => cache.shutdown();           // sync disposer
  }, 'cache');
}

```

The fiber tracks disposables in the `_disposables` stack (lines 12-14) and guarantees correct LIFO ordering, even with mixed sync and async disposers.

## Summary

- **Effect types** in Cordis accept `Promise<Disposable>` and `AsyncIterable<Disposable>` via the union type defined at lines 54-65 of [`fiber.ts`](https://github.com/cordiverse/cordis/blob/main/fiber.ts).
- **Execution handling** awaits Promises and iterates AsyncIterables in `_execute` (lines 29-72), storing results in a `DisposableList`.
- **Cleanup guarantees** are enforced by `_unload` (lines 37-48), which sequentially awaits async disposers in reverse registration order.
- **State tracking** uses `inertia` promises and `FiberState` enums (lines 99-115) to coordinate concurrent load/unload operations.
- **Error propagation** wraps async errors with `composeError` and transitions the fiber to `FiberState.FAILED` (lines 21-28).

## Frequently Asked Questions

### Can Fiber effects return a plain Promise for resource cleanup?

Yes. Effects may return `Promise<Disposable>` where the disposable itself can be synchronous or asynchronous. According to the implementation at lines 90-95, the fiber awaits the initial Promise, then stores the resulting cleanup function. During unload, if that cleanup function returns a Promise, the fiber awaits it before proceeding to the next disposable.

### How does Cordis handle errors during asynchronous cleanup?

Errors are caught in the `_unload` execution path, wrapped using `composeError`, and logged via `ctx.logger.error`. If an async disposer rejects, the fiber transitions to `FiberState.FAILED` and stores the error. The error is then re-thrown on the next call to `fiber.await()`, allowing plugins to handle cleanup failures explicitly while ensuring the unload sequence continues or aborts based on the error propagation logic in lines 21-28.

### What happens if an async effect is still loading when the fiber unloads?

The `inertia` promise tracks the ongoing operation via `_setEpoch` (lines 99-115). If `_unload` is called while the fiber is in `FiberState.LOADING`, the unload operation chains off the existing promise, ensuring that partial initializations complete or fail before cleanup begins. This prevents race conditions where a resource might be disposed before it fully initializes.

### Can I mix synchronous and asynchronous effects in the same plugin?

Yes. The `Effect` type union explicitly supports both `SyncEffect` and `AsyncEffect` patterns. The execution engine in `_execute` dynamically detects thenable objects versus immediate values, allowing synchronous iterables and async generators to coexist. The disposal stack at lines 12-14 handles both types uniformly, awaiting async disposers while invoking sync disposers immediately.