Reading the Effect docs instead of prompting
- YT :: https://www.youtube.com/watch?v=O2__t8lceCg
- Original title :: 🚨🚨 LEARNING EFFECTJS - Learning in the age of AI🚨🚨
A live learning stream: Prime works through the Effect v4 beta docs by hand, deliberately reading rather than prompting, with TJ joining near the end to explain the part the docs bury. The framing matters as much as the content — he already has an agent-generated Effect codebase he can't read, and the whole exercise is about refusing to ship code he doesn't understand.
The method: read the docs, don't ask the model
Chat repeatedly suggests using AI to learn faster and he repeatedly refuses. His reason is the maths-student failure mode: go fast because you're good at it, miss small concepts, and eventually hit an impenetrable wall built out of everything you skipped. He also names the trap directly — the false illusion of progress, where you see things move forward without understanding how they work, which caps you at whatever the tool can do on its own.
His concrete motivation is on screen. He has a client written from TJ's "how Effect should work" notes, full of layers, scopes, config, and provider wiring, and he can't read any of it. His litmus test for understanding generated code is being able to look at it and know what it does — and he fails that test, so he's going back to the beginning.
Working through the basics
The v4 release consolidates previously separate packages into core effect, with 17 unstable modules covering HTTP, AI, schema, SQL, RPC, CLI, workflows and clustering.
Effect<Success, Error, Requirements>— he guesses atRequirementsbefore reading it, and lands roughly right.- Effects are descriptions, not functions: immutable values that model effectful interactions and do nothing until run. A promise's constructor executes eagerly; an effect doesn't. His inference — that
run*belongs at a single top-level entry point which then interprets the whole tree — is confirmed by the docs. - Constructors:
succeed/fail(with tagged errors carrying a_tag),sync/tryfor synchronous work,promise/tryPromisefor async, andcallbackfor callback-style APIs. He detours to learn what a thunk is, having never encountered the term. syncmust not throw; if it does the error becomes a defect rather than a typed error — an unexpected crash, catchable separately.- Error recovery via
catchAll(erases the error type) andcatchTag(narrows it one variant at a time), which he likes. He does grumble that passing a class name as a string is an "insane" TypeScript-ism. suspenddefers creating an effect — "an effect that makes an effect".- Interruption and cleanup: with
yield*over parallel effects, a single failure interrupts the rest, andcallbackcan receive anAbortSignalso wrapped operations cancel properly. This lands with him because his own experience of hand-wiringAbortControllereverywhere is, in his words, the stuff of nightmares — and most TypeScript backends simply ignore the leak.
Two things he gets stuck on and flags honestly: generators (yield versus yield*, and how async works through them) — he concludes he needs to spend half an hour on them; and fork, which he can describe as green threads but not yet reason about.
Where he keeps landing is that the docs sell each feature individually and the individual features aren't convincing — "how is this different from a promise?" His own answer: Effect forces you to declare your errors, which alone removes a category of headache. He spends a fair chunk of the stream arguing that with chat, which he finds surreal, and TJ later agrees it's the least controversial part of Effect.
TJ on the part that isn't in the intro
TJ's diagnosis is that the payoff is in the third type parameter, which the onboarding path never reaches. His walkthrough:
- Requirements are dependency injection with teeth. Needing a logger alongside your data is normally solved with a global grab-bag; instead a service becomes part of the effect's type. If you never
provideService, it doesn't compile. Multiple services compose throughpipe; grouping them is what layers are for. - Scopes let an effect declare that it must run inside an acquired resource, making acquisition and release explicit — easy to malloc, hard to free.
- Never use bare
Erroras your error type. It's the parent of all errors, so it collapses the information Effect exists to give you, and defeats the point exactly the way an untyped throw does. - Testing gets much cheaper. A user service that depends on Postgres in production can be replaced in tests by a function returning objects, or one that fails with not-found — no mocking, no fixture data. And services that depend on other services need not leak that to the top level: build the connection inside the block and the outer type stops mentioning it.
- The type is a boundary against agents. TJ's stated reason for liking it most: he can look at a type and know the code cannot open a Postgres connection, so an LLM can't reach inside a loop and open the pool 85 times or run arbitrary SQL.
- Schemas over interfaces. In his codebase agents are forbidden from writing
interface; everything is a schema, becauseJSON.parse as MyTypeis a lie. This gets genuinely good when combined with HTTP API, where one shared endpoint definition — including the enumerated error cases — serves both sides, and you can derive a typed fetch client from a specification of someone else's API.
TJ's parting advice is to stop walking the guide: build the most minimal Bun HTTP API server, then a to-do app over SQLite/Drizzle, expose it via CLI first, then a frontend, and learn by hovering types.
How it ends
Prime cuts the stream short — a throat injury from about two years ago limits him to roughly two hours of talking. He's happy with the intro, concedes the stream is "barely a showcase" that shows the basics without showing what they're for, and plans a follow-up building an actual server so the requirements machinery has something to bite on.