Skip to content
You are reading v0.3, which is not the current release. Go to v0.4.

Effects and handlers

A Khora signature says what a function needs and how it can fail:

pub fn analyze(id: String) -> Report with { ledger: Ledger } raises DbError

Both rows are checked at compile time, and both compile to something ordinary. There is no effect interpreter, no handler stack to search, and no unwinder.

A capability row is static: the compiler knows at every call site exactly which handlers a function needs, because the signature says so and the checker has verified it.

So the handler is not looked up. It is passed in. The function above compiles to one taking an extra argument — the Ledger handler, which is a record of closures and therefore an ordinary heap value. Each call site supplies it from its own capability parameter, or from the enclosing with block.

Performing an operation is then a field read and a call through a function value. That is the same cost as calling any closure.

with { ledger: Ledger::real() } {
analyze(id) // `ledger` is handed to `analyze` as an argument
}

Nothing is searched at run time. There is no stack of installed handlers, no walking of frames to find the innermost one, and no cost that grows with nesting depth.

Row polymorphism ('ef) specialises the same way generics do. Khora monomorphises the whole program, so a row variable is concrete at every call site reachable from main for the same reason a type variable is.

A function whose raises row is non-empty returns two words: a tag and a payload. The tag is 0 when the call returned normally, and otherwise identifies the type of the error that was raised.

At each ! the compiler emits a branch. On the error path it releases what that frame owns and returns the tag upward, which is the same machinery an early return already uses.

This has consequences worth knowing:

There is no unwinder. No DWARF tables, no landing pads, no personality routine, no longjmp. A raise is a return with a tag, and each frame it passes through runs the releases it was going to run anyway. That is what keeps foreign frames out of the story — see FFI.

! is where the branch is. The mark a reader is taught to read as “control can leave here” is exactly the point where the compiler emits the test.

The tag names the error’s type, not just “something failed”. catch handles part of a row, so a function raising DbError + ModelError whose caller catches only ModelError has to know at run time which arrived. A variant index cannot answer that — DbError::Timeout and ModelError::RateLimited are both variant zero of their own types — so the tag is a program-wide id for the type, assigned once by the whole-program compiler. An error carries the same tag through every frame it crosses, whatever the rows in between look like.

The payload is one word because every Khora value is word-sized. Two registers in total, which is what a bare success/failure bit would have cost anyway.

A handler runs, returns, and control continues. It cannot capture the rest of the computation and resume it later, which is what a first-class continuation would allow.

Handlers are therefore single-shot, and the generator-shaped uses of algebraic effects are unavailable: an operation cannot yield more than once, and a handler cannot restart a computation. Stopping and continuing a computation is what a fiber does — see Fibers.

Because a handler never suspends, performing an operation costs a call rather than a stack switch, and a handler can be passed to foreign code without the foreign frames becoming part of anybody’s control flow.

Both are rows. Both are settled at compile time. Both are checked at every call and reported at the call rather than at the definition.

They compile to different things because they do different things: a capability is called and returns, so it is an argument; a failure leaves and does not, so it is a tag on the return. Keeping them separate in the syntax is what makes that difference legible.

Khora 0.3.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource