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

From TypeScript + Effect

If you know Effect, Khora’s motivation should feel familiar: failures and capabilities belong in types, resource lifetimes should be structured, and concurrent work should have ownership.

The major difference is where that model lives. Khora builds those ideas into a native language/runtime rather than expressing them as a TypeScript library over the JavaScript/Node execution model.

The one place the surface will read as less familiar than you expect. import is spelled the way you already spell it, but what marks a declaration public is pub:

pub type Entry = { id: Int, memo: String };
pub fn total(entries: List<Entry>) -> Int

It also appears on methods, which is why it is not export: nobody imports Map::get, they reach it by having a Map. A declaration without pub is private to its file, which is closer to a module with no export than to TypeScript’s default of exporting whatever is written at the top level.

Khora code calls effectful operations directly inside the capability/failure context declared by the function. There is no separate Effect<A, E, R> value that application code must construct and compose.

raises E corresponds roughly to the typed failure dimension. A with { ... } capability row corresponds to environmental authority. Handlers provide capabilities for a scope.

Effect<A, E, R> calls them Errors and Requirements. Khora writes them as two separate rows, and the conventional names are two letters each so you cannot read them backwards:

fn call<A, 'ef, 'er>(body: () -> A with 'ef raises 'er) -> A
with 'ef
raises 'er

'ef is the capability row — Effect’s R. 'er is the failure row — Effect’s E. A single-letter 'e was ambiguous in exactly the direction that hurts somebody arriving from Effect, which is why it is not spelled that way.

Effect Khora
Effect<A, E, R> a function’s return type, raises row and with row
Effect.either attempt
Redacted std::core::Redacted — same idea, and Show/Encode make it a compile error rather than a convention: a record holding one derives Decode and refuses Encode, where Effect’s Redacted encodes the secret back out
Config std::config::read over a Schema<A>, which derive(Decode) writes from the type; see below
Schedule, retry, repeat std::resilience
Clock.sleep, TestClock std::clock::Clock — sleep is an operation on the capability, so a fake clock is a handler and needs no fork-or-deadlock caveat
Queue with dropping/sliding Channel::dropping / Channel::sliding
Fiber.join Fiber::join, which re-raises the child’s failure with its type
Effect.forkScoped nursery.adopt(Fiber::spawn(..))
Ref Shared<A>
Layer nothing — see below

Some things are deliberately absent. Effect.gen, pipe, dual APIs, branded types and Match are TypeScript workarounds; Khora has methods, nominal types, match, and let.

Effect v3’s layer memoization could silently build an overlapping layer twice, and v4 shipped what its own guide calls a safety net for the footgun. The residual issue survives, because the memo key is object identity: calling a layer factory twice gives you two structurally identical layers built separately.

In Khora that is a let binding:

with {
config: env_config(),
scope: Scope::root(),
db: postgres_db()!,
store: sql_store(),
} {
run_server()!
}

Built once, in the order written, by the ordinary rules of a binding. There is nothing to memoize because there is nothing being rebuilt.

The Effect version of typed configuration is a description — a value denoting “read PORT as an integer”, interpreted later by a provider so a test can swap it. The whole description layer exists to defer the read.

Khora’s provider is the Env handler and it was already swappable, so the description is not there to defer the read. It is there for reuse: std::config::read(schema) walks a Schema<A> — the same one that reads a JSON body — for the variables it needs, and answers Validated, so every bad key is reported in one pass and each is spelled as the variable it came from.

Interruption is not only at effect boundaries

Section titled “Interruption is not only at effect boundaries”

In Effect, a tight loop inside one synchronous step cannot be interrupted. Khora separates scheduler safepoints from cancellation points and emits a safepoint at every loop back-edge, so a spinning loop is still cancellable. It looks like an implementation detail and it is not.

Khora’s |> is call-oriented. value |> f(a) means f(value, a), and a single _ placeholder can select another argument position. It is not limited to piping into unary functions.

Khora fibers and nurseries provide structured concurrency in the language/runtime. Cancellation and finalization remain central concepts, but source code stays ordinary direct-style Khora.

Khora compiles to native code and does not use a tracing GC or JavaScript VM. Automatic memory management is implemented through reference counting plus compiler ownership/reuse analysis rather than through a source-level borrow checker.

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