better-effect

Mental Model

Understand the four boundaries that make better-effect predictable.

A program is a value plus requirements

An Effect.gen program is not a class instance or a task scheduler. At runtime it is a Result (or a Promise of a Result). At compile time it additionally records the Services yielded by the generator.

Effect.gen(...) ──► Result<A, E> & { phantom requirements: R }


                     Runtime environment must provide R

The requirement channel is type-only. Service resolution still happens through the active resolver when the generator runs.

A Layer is a recipe, not an instance

Layer.make(Database, () => new Database(config)) stores an acquisition callback. It does not call the callback. A backend registers the provider, and the provider is acquired lazily the first time the Service is resolved.

This distinction keeps startup cheap and makes ownership explicit:

  • Layer.make describes lazy acquisition without a release callback.
  • Layer.succeed describes an already-created value.
  • Layer.scoped describes lazy acquisition plus Runtime-root cleanup.
  • Layer.gen and Layer.scopedGen can request other Services during acquisition.

Runtime is the long-lived owner

A Runtime owns a root Scope. Every runtime.run creates a child Scope. This gives request-local resources a short lifetime while keeping database pools or clients shared for the lifetime of the application.

Runtime root Scope
├── Layer.scoped(Database)
├── Layer.scoped(Cache)
└── execution child Scope
    ├── Effect.acquireRelease(request resource)
    └── Effect.add(already acquired disposable)

When disposal starts, new executions are rejected, active executions finish, the root Scope closes, and the backend is disposed last.

Result remains the control-flow model

Use Result.ok, Result.err and Result.await exactly as you do with better-result. Effect.gen delegates its runtime generator semantics to Result.gen; it only adds Service requirements to the inferred type.

Every Result.gen/Effect.gen generator must finish by returning a Result. Returning a raw value is not a successful completion of the generator contract.

On this page