Effect V4 in Practice: How to Actually Learn a TypeScript Library in the Age of AI

ThePrimeagen learns Effect V4 live, using "read and understand the code" as his AI acceptance test.
With AI generating code on demand, ThePrimeagen spent two hours live-learning TypeScript library Effect V4, using "being able to read the generated code" as his acceptance standard against the "false illusion of progress." Effect delivers three core layers of value: typed errors via `Effect<Success, Error, Requirements>` to compensate for TypeScript's inability to track `throw`; built-in structured concurrency that auto-cancels sibling requests on failure; and compile-time dependency injection that prevents services from secretly accessing databases. Developer TJ added practical guidance on using `Data.TaggedError` and Schema. Community pushback on "reinventing JS" also surfaced, but one point found consensus: knowing what errors can occur is genuinely useful.
At a time when AI coding tools are everywhere, a sharp question surfaces: if models can write code for you, why bother learning a new library the hard way? In a live coding stream, prominent developer ThePrimeagen spent over two hours reading documentation and experimenting hands-on with the TypeScript library Effect (V4 beta), and gave his answer. What looked like a loosely structured learning session turned out to reveal the core value of Effect — and exposed a fundamental divide in how people approach learning in the AI era.
Why You Still Need to Learn Libraries Hands-On in the AI Age
Right at the start of the stream, a viewer challenged him: with AI around, why do you still need to defensively justify learning a library? Primeagen's answer was blunt — because you still need to know how to use AI.
He made a sharp analogy: skimming documentation too fast and skipping over small concepts is like a math-savvy person rushing through the basics, only to hit a wall they simply can't get past — because too many small gaps have piled up. He deliberately slowed down, guessing what each concept meant as he read, then comparing his guess against the documentation's explanation.
His "AI-generated code acceptance criteria" was worth noting: he had AI (using TJ's configuration) scaffold an Effect project, but repeatedly emphasized, "I don't like living in a world where I don't know how this thing works." His acceptance test was simple — read the generated code with his own eyes, and pass it only if he understood it. Staring at a screen full of unfamiliar layer, scoped, provideMain, Sentry, and config, he said plainly: "I don't know what these do. If the translation looks right, then I need to go learn what it actually means."
This became the point he kept hammering to his audience: seeing things move forward without understanding why is a false illusion of progress — and you'll end up as a script kiddie, forever limited by how far your tools can take you.
Effect's First Layer of Value: Typed Errors
The Effect V4 documentation opens with a blunt critique of a longstanding TypeScript problem: "typed data, untyped programs." TypeScript is great at describing data shapes, but it tells you almost nothing about how a program can fail — a function signature doesn't communicate what errors it might throw. This is a "perma-issue" in TypeScript, likely to be closed as not planned.
Effect's core type is designed precisely around this gap: Effect<Success, Error, Requirements>, where the three type parameters represent the success value, possible errors, and the contextual dependencies required to run. Unlike a Promise, which only exposes the resolved value and swallows errors into unknown, Effect forces you to encode errors into the type system.

Primeagen got a hands-on feel for this through Effect Institute, an interactive learning tool built by Kit Langton. A typical checkout function calls three async operations internally, any of which can fail — but errors caught by try/catch are always unknown. You're left doing pattern matching based on assumptions gathered through "code archaeology," with no type-checker warning you when those assumptions are wrong. The line in the docs hits hard: "if you add a new error branch and forget to update your catch, congratulations — you'll be paged at 2am."
In Effect, once you throw a VeryBadRoll error with Effect.fail, the compiler will report a type error if you don't handle it, forcing you to "make peace with it." Adding a second error type is just a TypeScript union, and catchTag lets you handle a specific error precisely — handle one, and the error type narrows by one layer.
The fundamental reason TypeScript can't handle errors is that
throwstatements are "invisible" to the type system — function signatures can't declare what they throw. By contrast, Java has checked exceptions, Rust usesResult<T, E>, Haskell usesEither— all of these languages put error paths into the type system. Effect'sEffect<Success, Error, Requirements>is the same idea applied to the TypeScript ecosystem: it's a lazy value describing a computation, not an immediately-executed operation. This means an entire program can be built as a tree of effects, and nothing actually runs untilEffect.runMain(or another runner) is called. This design lets the type checker track all possible failure paths at compile time, rather than waiting until runtime to fish throughcatch (e: unknown).catchTagcan precisely narrow error types because eachTaggedErrorcarries a literal-typed_tagfield that TypeScript's narrowing mechanism can identify.
From succeed/fail to Concurrency and Resource Management
Primeagen walked through Effect's foundational constructors step by step: Effect.succeed (always succeeds), Effect.fail (represents a recoverable error), Effect.sync (synchronous side effects — the thunk can't throw, or it becomes a defect), Effect.try (potentially failing synchronous computation, like the classic JSON.parse), and the async equivalents Effect.promise / Effect.tryPromise and the callback-based Effect.callback.

What really caught his attention was structured concurrency. The documentation demonstrated launching multiple LLM requests in parallel: in the traditional Promise world, you'd have to manually wire up AbortController for cancellation — he admitted, "I have nightmares about AbortController." In Effect, if one request fails, the others are immediately interrupted. This capability is built in.
He also noted that Effect V4 merged a large number of previously separate packages into the core effect package. V4 ships with 17 unstable modules (covering HTTP, RPC, CLI, workflows, clustering, and more), aimed at reducing the cost of discovery, installation, and dependency synchronization.
"Structured Concurrency" is an important concept from systems programming, first systematically articulated by Nathaniel J. Smith in 2018, and popularized by Python's Trio library and Kotlin coroutines. The core claim is that concurrent tasks must have lifetimes strictly nested within the scope that created them — all child tasks must complete or be cancelled before the parent finishes, and any child failure propagates upward and triggers cancellation of sibling tasks. This stands in sharp contrast to the traditional "fire-and-forget" Promise model:
Promise.allwon't cancel running Promises when one fails — developers must manually wire inAbortController. Effect makes structured concurrency a built-in runtime guarantee, implemented withFiber(lightweight virtual threads), giving concurrent code predictable cancellation, timeout, and resource-release semantics — fundamentally eliminating the "ghost requests" and resource leaks common in the Promise world.
Dependency Injection: The Part TJ Finds Most Elegant
In the second half of the stream, developer TJ joined live and immediately pulled the conversation toward Effect's "third dimension" — Requirements (context/dependencies). TJ considers this the most exciting and valuable part of Effect.

Using the Random service as an example: you define a Service the same way you define an error type (there's a TypeScript idiom here that requires self-referencing in two places — even Effect author Kit Langton admitted "we wish we didn't have to do it this way"). Once you yield the service inside Effect.gen, the third type parameter of program will reflect that it depends on Random — if you don't provide Random, the code won't compile, not fail at runtime.
You can inject dependencies via Effect.provideService or by combining multiple services with pipe. TJ emphasized how powerful this is for testing and AI collaboration: your UserService in production depends on Postgres, but the interface is just getUser returning a user or a not-found error. In tests, you don't need to mock a pile of fake data — just write a minimal implementation that returns not-found. More critically — you can verify from the types that a given service won't secretly open a Postgres connection, which means AI can't "open and close a connection pool 85 times inside a for loop."

TJ also shared several practical recommendations:
- Never put a bare
Errorin your error types, becauseErroris the parent of all errors — it collapses your types into something that's always truthy, completely defeating Effect's purpose. The correct approach is to define named errors withData.TaggedError. - Use Schema instead of interface: he told his AI "writing an interface is illegal" — only Schema definitions are allowed, because you need to actually parse data rather than blindly trust whatever
JSON.parsereturns asany. - Schema combined with HTTP APIs is where it really shines — you can derive a client from a specification, get automatic serialization/deserialization, and get immediate type errors when field types don't match.
Effect's dependency injection mechanism is conceptually close to the Reader Monad in functional programming: encoding "what environment is required to run" into the type, rather than passing dependencies through global variables or implicit closures. In traditional TypeScript, dependency injection typically relies on IoC containers like InversifyJS, where dependencies are resolved at runtime and the type system can't verify them. Effect's Requirements parameter makes the compiler the guardian of dependency relationships — if an
Effect's third type parameter is notnever, it can't be run directly; dependencies must first be "filled in" viaprovideServiceorLayer.Layeris a high-level abstraction in Effect for managing service lifecycles: it describes how to build a service (including resource acquisition and release) and can be composed into a dependency graph. The Effect runtime automatically topologically sorts and initializes services on demand. In tests, you simply swap out aLayerfor a test implementation without touching any business logic.
Disagreements and Controversy: Is the Abstraction Worth It?
The stream also honestly captured community divisions. One viewer sharply commented that this looks more like "misleading engineering to paper over bad engineering" — runSync is just lazy execution of methods, "reinventing JS," handing CPU cycle management off to a machine. Others said they prefer Go's explicit error handling.
Primeagen expressed appreciation for Go's approach — error provenance is clear and straightforward — but noted that Go isn't as simple in JavaScript-style scenarios involving "aggregating and reusing a bunch of Promises." He was blunt about some syntax choices, calling them "cursed syntax" and "evil" — especially the generator semantics of yield vs yield* (one pushes out, one pulls in), and designs that use arity to determine which function overload to call.
On the one point that generated the least disagreement, both sides converged: knowing what errors can occur is useful. TJ found it almost funny — "this is the last thing in Effect that should be up for debate." As for the criticism that writing out all errors is verbose, he considered that a fair trade-off.
The stream ended early due to Primeagen's throat injury (a strange larynx injury from over two years ago — continuous speaking for more than two hours causes pain). He admitted the session was "barely a showcase" of what Effect can do — the parts that would really demonstrate its value, like building a server, a to-do app, and wiring up a Solid.js frontend, are saved for the next stream. TJ also suggested: rather than grinding through documentation, have AI generate a minimal bun HTTP server and start from a /health endpoint, letting the feel of type inference emerge through hands-on building.
Conclusion
This seemingly casual stream compressed a genuine learning picture for the AI era: AI can generate a code skeleton, but "being able to read and understand it" is the acceptance threshold. The more powerful the tools, the more understanding the underlying principles becomes a competitive moat — not a burden. Effect's three layers of value — typed errors, structured concurrency, and elegant dependency injection — each may have alternatives on their own, but combined they form a programming paradigm that makes it harder for AI to "go rogue" and easier for humans to confidently test edge cases. As Primeagen put it, the act of learning itself apparently needs to be "defended" these days — and that's precisely when it should be questioned the least.
Related articles

Professor Jiang's Three Terrifying Predictions: The Next 9/11 May Target Your Bank Account
Professor Jiang, who went viral for predicting the Iran war, argues the petrodollar system is collapsing. His three predictions: a 9/11-style attack on banks, a siege of NYC, and Messi becoming Argentina's president.

Are You Talking Too Much? A Communication Expert Breaks Down the Logic of High-Quality Conversation
A communication expert breaks down the signs of over-talking and the blueprint for great conversation — covering openers, archetypes, charisma, and graceful exits.

Signs of a Conversational Narcissist: Why Some People Only Talk About Themselves
Conversational narcissists always redirect talk back to themselves — rooted in insecurity, not arrogance. A survey of 4,000 people found the biggest social fear is awkward silence. Learn the signs and how assertive curiosity is the fix.