Handler Stacks

View Source

< Property-Based Testing | Up: Recipes | Index | LiveView Integration >

Handlers compose by piping the computation through with_handler. Each handler manages its own effect independently. Order mostly doesn't matter.

Stacking handlers

my_computation
|> State.with_handler(0)
|> Reader.with_handler(%{env: :prod})
|> Fresh.with_uuid7_handler()
|> Port.with_handler(%{Skuld.Repo.Effectful => MyApp.Repo.Port})
|> Throw.with_handler()
|> Comp.run!()

Production vs test

The same computation, different stacks:

# Production
comp |> State.with_handler(0) |> ... |> Comp.run!()

# Test
comp |> State.with_handler(0, tag: :test) |> Repo.InMemory.with_handler(...) |> Comp.run!()

Swap handlers, not code.

When order matters

The only time handler order is significant is when a handler wraps other handlers. EffectLogger must be innermost (first in the pipe) to record every effect invocation:

comp
|> EffectLogger.with_logging()     # innermost — records everything
|> State.with_handler(0)           #
|> Reader.with_handler(%{})        # these are recorded
|> Throw.with_handler()            #
|> Comp.run!()

Throw should be outermost (last) to catch errors from all inner handlers:

comp
|> State.with_handler(0)
|> Throw.with_handler()            # catches errors from State
|> Comp.run!()

Transaction should wrap the persistence layer but sit below Throw:

comp
|> State.with_handler(0)
|> Port.with_handler(%{...})
|> Transaction.Noop.with_handler()  # rollback for State + Repo
|> Throw.with_handler()             # catch errors
|> Comp.run!()

Per-effect handler reference

EffectProduction handlerTest handler
StateState.with_handler(comp, init)Same, swap value
ReaderReader.with_handler(comp, val)Same, swap config
WriterWriter.with_handler(comp, [])Same, output: captures log
ThrowThrow.with_handler(comp)Same, catch & inspect
FreshFresh.with_uuid7_handler()Fresh.with_test_handler()
RandomRandom.with_handler()Random.with_seed_handler(seed: 42)
PortPort.with_handler(comp, %{...})Port.with_test_handler(stubs)
RepoPort.with_handler(...) via EctoRepo.InMemory.with_handler(...)
FiberPoolFiberPool.with_handler(comp)Same
ChannelChannel.with_handler(comp)Same
TransactionTransaction.Ecto.with_handler(repo)Transaction.Noop.with_handler()
EffectLoggerEffectLogger.with_logging(comp)Same

< Property-Based Testing | Up: Recipes | Index | LiveView Integration >