I recently started learning Effect. Not because I had a project that required it, or because I had hit a limitation in TypeScript that I knew Effect would solve.
I kept seeing people talk about it on X. One idea in particular caught my attention: that Effect could help AI coding agents write more reliable code.
I don’t know yet how much that claim holds up. But it connected with a frustration I already had.
I’m always looking for ways to move faster with agents without losing confidence in what they produce. Even with good instructions, documentation, and tooling, I still find myself steering them back toward the way I want the code to be written.
That’s what made me take a closer look.
The code keeps drifting
I don’t just want my coding agents to write code faster. I want the code they produce to be reliable and consistent with the rest of the project.
An agent producing code quickly is useful. But I still have to read it, understand its choices, and check whether those choices fit the rest of the project.
The recurring frustration is drift. It happens when an agent adds something new, and it happens when it modifies existing code. The patterns I wanted it to follow aren’t consistently the patterns I get back.
It’s hard to reduce this to one spectacular failure. It’s more the ongoing need to check. Instructions help. Documentation helps. A good harness helps. But I still have to supervise the consistency of the implementation.
So the question isn’t just how to get an agent to write more code. It’s how to keep that code moving in a consistent direction without intervening all the time.
Can some instructions become constraints?
There’s a difference between asking an agent to follow a convention and having the development tools reject code that breaks a rule.
Where possible, I’d rather have expectations checked than rely on the agent remembering and applying them every time.
Not everything can become a compiler error. A type checker can’t decide whether an abstraction is useful or whether the implementation matches what I actually wanted to build. But I’m curious about how much we can move in that direction.
That’s the question I’m bringing to Effect.
I’m still learning the basics, using Effect v4. But a few small discoveries have already made that question more concrete.
Errors are part of the signature
One of the first things I learned was how to read the Effect type:
Effect<Success, Error, Requirements>A Promise<User> tells me what a successful result looks like. It doesn’t encode the type of a rejection. Effect also has a place for expected errors and the requirements a program needs to run.
That changes what I can see just by reading a signature.
Something else clicked when I learned about catchTag: handling one tagged error doesn’t make the other errors disappear. The remaining expected errors stay visible in the type, alongside any new errors introduced by the handler.
For reviewing agent-written code, that seems useful. More of the contract is available without tracing every path through the implementation.
But I also had to correct an early misunderstanding: an error channel of never doesn’t mean the program can never crash. It means there are no expected errors represented in that channel. Unexpected defects can still happen.
Effect doesn’t force every expected error to be recovered from, either. Errors can propagate to the application boundary. What interests me is that they remain visible while composing the program, not that every failure magically disappears.
A resource needs someone to own its lifetime
Resource management gave me the clearest example of an expectation becoming a checked requirement.
When I learned acquireRelease, I discovered that it adds Scope to the requirements channel. The release operation is registered with that scope, and the resource’s lifetime is tied to it.
My initial intuition was wrong: I thought acquireRelease was simply a shorter, self-contained version of acquireUseRelease. Instead, acquireUseRelease manages the acquire-use-release lifecycle within one operation, while acquireRelease lets the resource be used across steps within a surrounding scope.
That distinction shows up in the types.
Using the normal typed runners, I can’t just run a program with an unsatisfied scope requirement. Something has to provide it, for example, Effect.scoped.
Here’s a small example with a mock connection:
import { Effect } from "effect"
const connection = Effect.acquireRelease(
Effect.succeed({ name: "database" }),
(resource) => Effect.log(`Closing ${resource.name}`)
)
// Effect.runPromise(connection) // Type error: Scope is missing
const program = Effect.gen(function* () {
const resource = yield* connection
yield* Effect.log(`Using ${resource.name}`)
})
await Effect.runPromise(Effect.scoped(program))
// Logs "Using database", then "Closing database"The object and log stand in for opening and closing a real connection. Effect.scoped supplies the scope and runs its registered finalizers when that scope closes.
That’s the kind of structure I’m looking for. Rather than only documenting “remember to manage the resource lifetime,” the API makes ownership of that lifetime a requirement.
It doesn’t prove I chose the right lifetime or wrote the correct release logic. But it gives the compiler something concrete to check.
Cleanup is not just the last step
Another learning moment started with a simple question: why not just put cleanup at the end of the pipeline?
Because “at the end” only works if execution reaches it.
A normal next step gets skipped when an earlier operation fails. Registering a finalizer with Effect.ensuring expresses something different: run this cleanup whether the operation succeeds, fails, or is interrupted.
This small example makes the difference visible:
import { Effect } from "effect"
const work = Effect.fail("Something went wrong")
const cleanup = Effect.log("Cleanup ran")
const cleanupAsNextStep = work.pipe(Effect.andThen(cleanup))
const cleanupAsFinalizer = work.pipe(Effect.ensuring(cleanup))
await Effect.runPromiseExit(cleanupAsNextStep) // No cleanup log
await Effect.runPromiseExit(cleanupAsFinalizer) // Logs "Cleanup ran"Both runs still fail. runPromiseExit returns those outcomes as values so we can run both examples without a rejected promise stopping the script. ensuring runs the finalizer; it doesn’t recover from the error.
That helped me distinguish work on the success path from work attached to an operation’s lifetime.
This isn’t about stronger types alone. It’s about explicit runtime behavior. Nor is it something ordinary TypeScript can’t express: try/finally already exists. What interests me is having a consistent set of APIs for expressing these expectations while composing effects.
The runtime runs the registered finalizer; I still have to supply cleanup that actually does the right thing. And this isn’t a guarantee against things like the process being forcibly killed.
For me, the useful part is making the intention precise rather than leaving cleanup as an ordinary step that happens to sit at the bottom of a function.
A reason to investigate, not a conclusion
These examples haven’t proved that my agents will write better code with Effect.
I haven’t measured a reduction in review time. I haven’t shown that Effect prevents the drift I see in my projects. More structure could help, but it also gives both me and the agent more to learn.
An agent can still choose an unhelpful abstraction, model the wrong errors, or write incorrect logic inside a well-typed program.
That’s also why I want to learn Effect myself. If I’m choosing a tool partly because it might help my agents, I need to understand it well enough to judge what they produce. I don’t want to replace checking familiar code with trusting unfamiliar code.
Still, my reason for exploring it is now more specific than “people on X say it’s good for AI.” Expected errors are visible in signatures. Resource lifetimes become requirements. Cleanup has explicit semantics.
I want to find out whether those properties can reduce how much I have to keep reminding agents, and myself, to get right.
The goal isn’t to stop reviewing code. It’s to spend less of that review repeating the same corrections.
AI coding agents gave me the reason to start learning Effect. Now I want to understand whether it can help with the part that still feels difficult: not generating code, but keeping it consistent as a project evolves.