Build an AI Agent with Effect.ts: Typed Errors & Retries (2026)
By Sagar Shankaran, Founder of CallSphere
Effect.ts v3 makes every LLM failure a typed effect channel. Wire OpenAI calls with Schedule retries, fallback layers, and Cause inspection — no try/catch.
Key takeaways
TL;DR — Effect.ts v3 (production at Vercel, Prisma) makes errors first-class in the type system. Combine
Effect.retrywithSchedule.exponentialand a fallback Layer and your AI agent gracefully degrades from gpt-4o → gpt-4o-mini → cached answer.
What you'll build
An "answer service" with three failure modes — RateLimitError, TimeoutError, ContentFilterError — each surfaced as a typed channel. The service retries 3x on rate-limits, falls back to a cheaper model on timeouts, and short-circuits on content filter.
Prerequisites
effect@^3.10,@effect/ai@^0.6,@effect/ai-openai@^0.6, Node 20+ or Bun.
Architecture
flowchart TD
Q[Question] --> P[Effect program]
P --> R{retry policy}
R -->|429| RT[exp backoff x3]
R -->|503/timeout| FB[fallback Layer mini]
R -->|content filter| CR[Cause.fail]
RT --> R
FB --> O[reply]
Step 1 — Define typed errors
```ts import { Data } from "effect";
export class RateLimitError extends Data.TaggedError("RateLimit")<{ retryAfter: number }> {} export class TimeoutError extends Data.TaggedError("Timeout")<{ ms: number }> {} export class ContentFilter extends Data.TaggedError("ContentFilter")<{}> {} ```
Step 2 — Wrap OpenAI as an Effect
```ts import { Effect, Schedule, Duration } from "effect"; import OpenAI from "openai"; const oa = new OpenAI();
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
export const ask = (q: string, model: string) => Effect.tryPromise({ try: () => oa.chat.completions.create({ model, messages: [{ role: "user", content: q }], }), catch: (e: any) => { if (e?.status === 429) return new RateLimitError({ retryAfter: 2 }); if (e?.code === "ETIMEDOUT") return new TimeoutError({ ms: 30000 }); if (e?.error?.code === "content_filter") return new ContentFilter(); return e; }, }).pipe(Effect.map((r) => r.choices[0].message.content ?? "")); ```
Step 3 — Compose with retry + fallback
```ts import { pipe } from "effect";
const retryOnRate = Schedule.exponential(Duration.seconds(1)) .pipe(Schedule.intersect(Schedule.recurs(3)), Schedule.whileInput((e: any) => e._tag === "RateLimit"));
export const answer = (q: string) => pipe( ask(q, "gpt-4o"), Effect.retry(retryOnRate), Effect.catchTag("Timeout", () => ask(q, "gpt-4o-mini")), Effect.catchTag("ContentFilter", () => Effect.succeed("I can't help with that.")), ); ```
Step 4 — Run + observe Cause
```ts import { Effect, Cause } from "effect";
const result = await Effect.runPromiseExit(answer("hello")); if (result._tag === "Failure") console.error(Cause.pretty(result.cause)); ```
Step 5 — Tracing
```ts import { NodeSdk } from "@effect/opentelemetry"; const Tracing = NodeSdk.layer(() => ({ resource: { serviceName: "ai-agent" }, spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter()), })); Effect.runPromise(answer("...").pipe(Effect.provide(Tracing))); ```
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Step 6 — Add a fallback Layer
```ts import { Layer } from "effect"; import { OpenAiClient } from "@effect/ai-openai";
const PrimaryAI = OpenAiClient.layer({ apiKey: process.env.OPENAI_API_KEY! }); const FallbackAI = OpenAiClient.layer({ apiKey: process.env.AZURE_KEY!, baseUrl: "https://eastus.openai.azure.com" }); const AI = Layer.orElse(PrimaryAI, () => FallbackAI); ```
Pitfalls
- Async/await mental model: Effect generators (
Effect.gen) read like async/await but yield Effects — new contributors hit confusion fast. - Bundle size:
effectcore is ~50kb min+gzip. Tree-shaking is good but watch lambda cold start. - Schedule precedence:
Schedule.intersectANDs,unionORs — easy to mix up.
How CallSphere does this in production
Explore a live demo and compare current plans to find the right fit for your business.
FAQ
Why not try/catch? Try/catch loses the error type at the function boundary — Effect keeps it.
Can I incrementally adopt? Yes. Convert one service to Effect, return Promises at the edge with Effect.runPromise.
Performance overhead? ~5-15µs per Effect step, dwarfed by network latency.
Effect.ts vs fp-ts? Effect supersedes fp-ts in 2026 — fp-ts maintainer joined Effect team.
Sources
- Effect-TS docs - https://effect.website/
- Effect-TS AI - https://deepwiki.com/Effect-TS/effect/10-ai-and-external-services
- Noqta - Effect-TS 2026 tutorial - https://noqta.tn/en/tutorials/effect-ts-typescript-error-handling-pipelines-2026
- Dev.to - Effect-TS in 2026 - https://dev.to/ottoaria/effect-ts-in-2026-functional-programming-for-typescript-that-actually-makes-sense-1go

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.