Navigation

Effects and Handlers

nix-effects separates program intent from execution policy. A program sends operations such as "read state", "write state", or "report a type error". A handler decides what those operations mean.

That split is the execution model for the rest of the library. Typed validation, diagnostics, streams, resource tracking, and the checker all use the same effect substrate. The validator does not decide whether an error aborts immediately or gets collected with the next ten errors. The handler decides.

Computations

A computation is either finished or waiting for a handler:

Pure value
Impure effect continuation

The public constructors are pure, send, and bind:

let
  inherit (fx) pure bind send;
in
  bind (send "get" null) (state:
    bind (send "put" (state + 1)) (_:
      pure state))

Most code uses effect modules rather than raw send:

let
  inherit (fx) pure bind run;
  inherit (fx.effects) state;

  increment =
    bind state.get (n:
      bind (state.put (n + 1)) (_:
        pure n));
in
  run increment state.handler 41

The result contains the returned value and final handler state:

{ value = 41; state = 42; }

Handlers are policy

A handler is an attrset of operations. Each operation receives the effect parameter, the current handler state, and the continuation protocol. It can resume the computation or abort it:

{
  get = { param, state }: {
    resume = state;
    inherit state;
  };

  put = { param, state }: {
    resume = null;
    state = param;
  };
}

The computation above never mentions how state is represented. It only sends get and put. That is why the same validation logic can run under a strict handler in CI, a collecting handler in a documentation test, or a logging handler in a development shell.

Type checking as an effect

Validation uses the same shape. A type can expose both:

  • .check value for a fast boolean boundary.
  • .validate value for an effectful check with diagnostics.
let
  inherit (fx.types) String refined;
  TargetClass = refined "TargetClass" String
    (x: builtins.elem x [ "module" "file" "package" "check" ]);
in
  fx.run (TargetClass.validate "fleet")
    fx.effects.typecheck.collecting []

The type sends a typeCheck request with context. The handler turns that request into an error list, a thrown exception, or a trace. The type does not need separate implementations for each policy.

Composition

Handlers compose because computations are ordinary values. adapt and adaptHandlers let a local computation run under a different handler view without rewriting the computation itself. Streams use the same substrate to request the next value lazily. Linear resources use it to count consumption. The kernel uses it to report type errors without placing diagnostics inside the trusted evaluator.

The implementation details live in the Trampoline and Systems Architecture chapters. For day-to-day use, the rule is simple: write computations in terms of operations, then choose the handler that matches the boundary where the computation runs.