Skip to content
Logo

Running effects

Effects are inert values; a runner interprets one. The sync and async runners come in a throwing form that returns the value and an _exit form that returns an Exit. Use run_main at a process entry point to report failures and choose exit codes:

RunnerRunsReturns
run_sync(effect)synchronouslythe value, raising on failure
run_sync_exit(effect)synchronouslyExit[A, E]
run_async(effect)on a fresh asyncio loop (asyncio.run inside)the value, raising on failure
run_async_exit(effect)on a fresh asyncio loop (asyncio.run inside)Exit[A, E]
run_main(effect)on a fresh asyncio loopthe value, logging and exiting on failure
await run_async_coroutine(effect)inside a loop you already ownExit[A, E]

run_sync and run_async raise a typed failure as the error itself (every EffectonError is an Exception), re-raise an exception defect as it is, wrap any other defect in UnhandledDefect, and re-raise the exception carried by an interruption:

try:
     = E.()  # A
except  as :  # a typed failure
    ...

The _exit forms never raise; they return an Exit to match on:

match E.():  # Exit[A, E] = Succeeded[A] | Failure[E]
    case E.(value):
        ...
    case E.(cause):
        ...  # cause is Fail(error) for typed failures, Die(defect) for unexpected exceptions, Interrupt(exception) for cancellations

run_async and run_async_exit interpret the same effect under asyncio, awaiting every coroutine effect they reach. They own the event loop through asyncio.run, so they cannot be called from a running loop; run_async_coroutine is the coroutine underneath, for a caller that already has one:

async def () -> None:
     = await E.(
        
    )  # Exit[A, E], awaiting coroutine effects along the way
    ()

Running an effect that contains a coroutine effect synchronously doesn't await it: run_sync_exit settles as Failure(Die(AsyncEffectInSyncRun())), run_sync raises AsyncEffectInSyncRun, and finalizers still run in both cases.

More examples: test_run_sync.py, test_run_async.py.