Documentation for agents: an index at /llms.txt, every docs page in one file at /llms-full.txt, and Markdown for any docs page by adding .md to its URL.

Introducing Effect Kit

Our first two packages bring Redis and email into Effect, with errors that say how far a request got before it failed, so you know when to retry.

4 min read

An isometric app plate with Redis and Emails blocks docked in their slots and an empty slot for Storage

Effect Kit is a set of open-source packages that bring the infrastructure your app depends on into Effect.

This week we published the first two: @effect-kit/redis and @effect-kit/emails.

The question a Promise can't answer

If you talk to Redis or send email from Effect today, you probably wrap a Promise client in Effect.tryPromise. That works well, right up until a call fails halfway.

The connection drops while you're writing. Did Redis apply the INCR? The email provider times out. Is the customer about to get two receipts?

The wrapped Promise can't tell you. It knows the request failed. It doesn't know how far it got, and that is the one thing you need to decide what to do next.

We built Effect Kit around that question.

Every failure says how far it got

In @effect-kit/redis, a dropped connection fails with a ConnectionError, and its delivery field says where the command was when it happened.

"unsent" means nothing was written. The server never saw the command, so sending it again is safe.

"possibly-written" means the bytes may have reached the server, but no reply came back. The command may have run once, or not at all.

The client never sends a command twice on its own. Retrying is your call, and the error carries what you need to make it:

import { ,  } from "effect";
import {  } from "@effect-kit/redis";

const  = .(function* () {
  const  = yield* ;
  return yield* .("order:42", "paid");
}).(
  .({
    : .("200 millis"),
    : 5,
    : () => . === "ConnectionError" && . === "unsent",
  }),
);

Here's that same situation as a machine you can break. Throw the switch before the write, then after it, and watch the error change.

SET order:42 paid is queued in the client.
SET order:42 paidqueued637901234567890012345678900123456789001234567890
Cut the connection before or after the write. Only one of them is safe to retry.

The difference matters more for some commands than others. Setting order:42 to "paid" twice is harmless. Incrementing a counter twice is a bug, and nothing in a plain error message will tell you which case you're in.

Email has the same problem with a different name. Every EmailError from @effect-kit/emails carries an acceptance field:

  • "notSubmitted": nothing reached the provider. Send it again.
  • "rejected": the provider refused it. Sending the same message again won't help, so fix it first.
  • "unknown": the provider may have accepted it. Retry only with an idempotency key on a provider that honors one.

Some failures never need a request at all. Each provider declares what it can carry, and email.send checks the message against that before calling the provider. Send an attachment through MailerSend and you get an UnsupportedFeatureError with acceptance: "notSubmitted", before anything leaves your process.

Swap the Layer, keep the program

Each package gives you one service, Redis or Email, and a set of Layers that provide it. Your program only ever asks for the service:

import {  } from "effect";
import {  } from "@effect-kit/emails";

const  = (: string) =>
  .(function* () {
    const  = yield* ;
    return yield* .({
      : "hello@example.com",
      ,
      : "Welcome",
      : "Your account is ready.",
    });
  });

Where the email actually goes is decided once, at the edge of the app:

.(.(.()));
.(.(.()));
.(.(.()));

Moving from Resend to Postmark is a one-line change. So is a test: EmailTest.layer() runs the same validation and capability checks as a real provider, then records the message in an outbox you can assert on instead of sending it.

Redis works the same way across runtimes. RedisNode.layer and RedisBun.layer open native sockets, and RedisUpstash.layerConfig talks HTTP for places without them, like Deno and Cloudflare Workers.

yield*
Turn a knob to swap a Layer. The program in the middle never changes.

Try it

Both retry examples are in the playground below, under the Effect.retry tab of each package, next to pipelines, scopes, batches and capability checks. They run against a simulated Redis and a simulated email provider, so you can pick where things fail.

Send
SET
INCR
GET
replies

Simulated Redis: every round trip takes 400ms.

const name = yield* redis.set("{user:42}:name", "Ada");
const visits = yield* redis.incr("{user:42}:visits");
const plan = yield* redis.get("{user:42}:plan");

Inspired by Kit Langton's Visual Effect

Everything runs in your browser. Nothing here talks to a real server.

What's in the first release

Redis. A native client that speaks RESP itself, so Effect is its only dependency. Commands are typed, connections are scoped and close with your program, and it covers pipelines, transactions, Pub/Sub, Streams, Cluster and Sentinel. It runs against Redis, Valkey, Dragonfly, KeyDB, Redict, Garnet, Apache Kvrocks and Upstash. It's already on 0.1.1, which added typed batch and reusable compile plans. The quick start takes a few minutes.

Emails. One Email service across 16 providers, including Resend, Amazon SES, Postmark, SendGrid, Mailgun and SMTP. You install only the SDK for the provider you use. EmailReact.render turns a React Email component into HTML and plain-text bodies, and email.sendMany sends up to 1,000 messages with bounded concurrency, returning one result per message. Start with the quick start.

What's next

Storage is next: put, scoped get, head, delete and paginated listing, in the same shape as the other two. One service, a Layer per backend, and errors you can act on.

Effect Kit is Apache-2.0 and developed in the open on GitHub. If something is missing or broken, open an issue. Updates go out on X at @effectkit and here on the blog.

Thanks for reading. Now go break a connection on purpose.