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 valuefor a fast boolean boundary..validate valuefor 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.