Effect notes: PRs, progress, and joys
It's been three months since the last Effect devlog and I'm still incrementally adopting Effect in Val Town.
Things are going well but not spectacularly. My approval rating is a solid 'B' right now.
I'm far from the only or most important Effect user, but I'm bummed that a majority of my annoyances from October & November of 2025 are outstanding: a drizzle bug, a Cron bug, a vitest incompatibility, documentation improvements are all stalled. I write a brief fork that implemented Cron iteration in the reverse direction, Cron.prev, and Kit finished and merged it, but that took three months to get merged and has been stalled without release for another three months. The documentation for Effect.fn.Return got written by a core team member but that PR got closed without merging. I tried writing some docs for stream interop which never got merged or reviewed. I got a minor documentation improvement about generators merged with explicit coordination from the Effect team.
None of that stuff is a dealbreaker and I know from privately chatting with the Effect team that they prefer PRs to be coordinated because there's so much in flight and so much to understand. I think the current scenario, in which people don't know that because it isn't written anywhere, and put in effort into PRs that stall, isn't very good for community vibes.
Probably some of this delay is because Effect v4 has been a major focus of the Effectful team. v4 does seem exciting: a smaller, more unified, faster module is great news. We haven't migrated yet because we use some of the deprecated APIs, like Runtime, and I try to avoid using beta releases in general in production software.
Obligatory LLM discourse: my usual AI tool, Claude Code with Opus 4.6, is decent at using Effect but stumbles in the same places that the documentation is lacking, like that Effect.fn.Return type - it doesn't like to use that. I've used the LLM for roughly half of the refactors to Effect, but recently I've been finding it slower than doing things manually along with ast-grep. There are faster LLMs, but I've found quality and speed to be strongly inversely correlated, and fast models like Minimax tend to get themselves into corners faster.
Effect joys
The Duration and DateTime modules have been really nice for describing times and limits in the application. Soon, Duration will do a lot of the same things that those Effect utilities can do, but it's nice to have them a little early.
We have a lot of Drizzle queries that try to fetch one record, so .limit(1) and then .pipe(Effect.map(r => r.at(0)) and I recently created a nice little dual method as a helper. This was not especially easy to write!
/**
* For the many many database queries where we want to take the first
* element and return NotFoundError if it is not found.
*
* @example
* db.query(...)
* .pipe(takeFirst('Project not found'));
*/
export const takeFirst = dual<
(
that: string
) => <T, Error, Requirements>(
self: Effect.Effect<T[], Error, Requirements>
) => Effect.Effect<NonNullable<T>, Error | NotFoundError, Requirements>,
<T, Error, Requirements>(
self: Effect.Effect<T[], Error, Requirements>,
that: string
) => Effect.Effect<NonNullable<T>, Error | NotFoundError, Requirements>
>(2, (self, message = "Not found") => {
return Effect.flatMap(self, (value) => {
const first = value.at(0);
if (first !== undefined && first !== null) {
return Effect.succeed(first);
}
return Effect.fail(new NotFoundError({ message }));
});
});The more methods get ported to Effect the more I can use Effect.gen or Effect.fn to combine them, which is nice - feels like a tipping point in many ways. This is something that I have noticed that LLMs are hesitant to do: they're pretty single-minded when working on a task and will happy let two Effect.runPromise statements sit on consecutive lines when they could be combined.
The friction of Effect at boundaries is still there: Val Town uses Fastify, tRPC, React Router, etc., and we have a lot of existing code, so we aren't achieving the brilliant purity that Effect docs insist upon. Tests still don't use @effect/vitest because of its missing feature and lack of vitest 4 support, so there's a lot of unwrapping Effects there too.
Overall: it rolls on, and I'm starting to slowly introduce Effect to the tough core of the application, the part that actually runs vals, and has many complex asynchronous flows. That is going fairly well: the last push was to replace our homemade implementation of Promise.withResolvers() that also had a Bun-inspired .peek() method to synchronously get a Promise value if there is one. Effect's Deferred was a drop-in replacement for that problem area.