Home / Part 2
Part 2 — The how
How it works under the hood
Part 1 ended on one claim: an Effect is a description, not a running program. Now we make that literal — we build the description, and we build the loop that walks it. By the end you'll have built every piece yourself.
Part 2 builds a small Effect runtime from scratch. Each section is a folder
with a self-contained runtime.ts and a runnable example.ts — read
them, run them with bun, and step through them with a debugger as you read.
We rebuild a small, fully-typed version of Effect across seven sections. The first six (2.1–2.6) build the runtime, each adding to the same engine; the last (2.7) uses it to write a real program:
One object, two views
One structural idea makes all of this work, so it's worth getting straight up front.
To you, an Effect is an abstract type. You only care about three things about it:
the value it produces, how it can fail, and what it needs — A, E,
R. At runtime, the same value is a plain object with an _op tag and
whatever fields the interpreter needs to run that node. The constructors build the object and
label it as an Effect<A, E, R>; the interpreter reads it back as a tagged
node, casting between the two views.
// carries only A, E, R.
// no fields, so inference stays clean.
interface Effect<out A, out E, out R> {
readonly _op: string
}
// _op says which node; the rest is what running it needs.
type Primitive =
| { _op: "Succeed"; value: unknown }
| { _op: "Failure"; error: unknown }
| { _op: "Sync"; thunk: () => unknown }
| { _op: "OnSuccess"; self: Effect; f: (a) => Effect }
// ... more nodes get added each section
So the surface stays fully typed while the interpreter works on plain tagged objects. Real Effect is built the same way. With that in mind, every section is just "add a node type, add a case to the loop."
The seven sections
Foundations
An Effect is data, and a loop runs it. Constructors, sequencing, and the continuation stack.
Async
How the loop pauses on a promise and picks back up later — with no await in your code.
Context
Dependencies as data, and where the R type parameter comes from.
Error handling
How failure short-circuits the happy path, and how one node powers catch, branch, and retry.
Concurrency
The Fiber: more than one walk of the tree at once, waiting, racing, and cancelling.
Ergonomics
The sugar that makes the API read top-to-bottom instead of inside-out — gone before runtime.
Putting it together
The finale: a real concurrent, retrying, dependency-injected program, built entirely from the toy.
Every node we build maps to a real op-code in the Effect source
(OP_SUCCESS, OP_ASYNC, OP_ON_FAILURE, …). Where our toy
simplifies something, the section says so.