Effect is a TypeScript library for building type-safe, composable programs where errors, dependencies, and concurrency are first-class values. Effect<Success, Error, Requirements> encodes what a computation produces, what errors it can raise, and what services it needs. pipe() chains operations without nesting. Effect.gen with yield* reads like async/await but works for any effect. Layer composes dependency injection. Schema provides runtime validation with full TypeScript inference. Stream handles async sequences with backpressure. Claude Code generates Effect programs, service layers, schema validators, and stream pipelines for production TypeScript applications with precise error types.
CLAUDE.md for Effect Projects
## Effect Stack
- Version: effect >= 3.10, @effect/schema >= 0.73 (now @effect/schema bundled in effect)
- Core: Effect<A, E, R> — Success, Error, Requirements tritype
- Composition: pipe() for linear chains, Effect.gen for imperative-style with yield*
- DI: Layer<Services, Error, Requirements> — compose into final layer
- Schema: Schema.decodeUnknown for parsing, Schema.encode for serialization
- Concurrency: Effect.all (parallel), Effect.forEach, Fiber management
- Error: typed tagged errors (class MyError extends Data.TaggedError<"MyError">)
Core Effect Patterns
// src/effects/core.ts — fundamental Effect patterns
import { Effect, pipe, Data, Option, Either } from "effect"
// Tagged errors: precise error types with tags
class OrderNotFoundError extends Data.TaggedError("OrderNotFoundError")<{
orderId: string
}> {}
class DatabaseError extends Data.TaggedError("DatabaseError")<{
message: string
cause?: unknown
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
field: string
message: string
}> {}
// Effect<Order, OrderNotFoundError | DatabaseError, never>
// = returns Order, might fail with these errors, no services required
const getOrderById = (orderId: string): Effect.Effect<Order, OrderNotFoundError | DatabaseError> =>
Effect.tryPromise({
try: async () => {
const order = await db.orders.findUnique({ where: { id: orderId } })
if (!order) throw new OrderNotFoundError({ orderId })
return order
},
catch: (error) => {
if (error instanceof OrderNotFoundError) return error
return new DatabaseError({ message: String(error), cause: error })
},
})
// pipe: compose operations left to right
const processOrder = (orderId: string) =>
pipe(
getOrderById(orderId),
Effect.flatMap((order) =>
order.status === "pending"
? Effect.succeed(order)
: Effect.fail(new ValidationError({
field: "status",
message: `Cannot process order with status: ${order.status}`,
}))
),
Effect.tap((order) => Effect.log(`Processing order: ${order.id}`)),
Effect.flatMap((order) => updateOrderStatus(order.id, "processing")),
)
// Effect.gen: imperative style with full type safety
const createOrderEffect = (
customerId: string,
items: OrderItem[]
): Effect.Effect<Order, DatabaseError | ValidationError, OrderService | EmailService> =>
Effect.gen(function* () {
const orderService = yield* OrderService
const emailService = yield* EmailService
if (items.length === 0) {
yield* Effect.fail(new ValidationError({ field: "items", message: "Required" }))
}
const order = yield* orderService.create(customerId, items)
yield* emailService.sendConfirmation(order)
yield* Effect.log(`Order created: ${order.id}`)
return order
})
Service Layers
// src/services/order-service.ts — Effect service pattern
import { Effect, Layer, Context, pipe } from "effect"
// Service interface
class OrderService extends Context.Tag("OrderService")<
OrderService,
{
create: (customerId: string, items: OrderItem[]) =>
Effect.Effect<Order, DatabaseError>
findById: (id: string) =>
Effect.Effect<Order, OrderNotFoundError | DatabaseError>
cancel: (id: string) =>
Effect.Effect<Order, OrderNotFoundError | DatabaseError | ValidationError>
}
>() {}
class EmailService extends Context.Tag("EmailService")<
EmailService,
{
sendConfirmation: (order: Order) => Effect.Effect<void, never>
sendCancellation: (order: Order) => Effect.Effect<void, never>
}
>() {}
class DatabaseService extends Context.Tag("DatabaseService")<
DatabaseService,
{ query: <T>(sql: string, params: unknown[]) => Effect.Effect<T[], DatabaseError> }
>() {}
// Layer: provides a service implementation
const OrderServiceLive = Layer.effect(
OrderService,
Effect.gen(function* () {
const db = yield* DatabaseService
return OrderService.of({
create: (customerId, items) =>
Effect.gen(function* () {
const totalCents = items.reduce(
(sum, item) => sum + item.priceCents * item.quantity,
0
)
const [order] = yield* db.query<Order>(
"INSERT INTO orders (customer_id, total_cents, status) VALUES ($1, $2, 'pending') RETURNING *",
[customerId, totalCents]
)
return order
}),
findById: (id) =>
pipe(
db.query<Order>("SELECT * FROM orders WHERE id = $1", [id]),
Effect.map(([order]) => order),
Effect.flatMap((order) =>
order
? Effect.succeed(order)
: Effect.fail(new OrderNotFoundError({ orderId: id }))
)
),
cancel: (id) =>
Effect.gen(function* () {
const order = yield* OrderService.findById(id)
if (order.status !== "pending") {
yield* Effect.fail(new ValidationError({
field: "status",
message: `Cannot cancel order with status: ${order.status}`,
}))
}
const [updated] = yield* db.query<Order>(
"UPDATE orders SET status = 'cancelled' WHERE id = $1 RETURNING *",
[id]
)
return updated
}),
})
})
)
// Compose layers into a full application layer
const AppLayer = Layer.mergeAll(
OrderServiceLive,
EmailServiceLive,
DatabaseServiceLive,
)
// Running effects with a layer
const program = createOrderEffect("cust-123", items)
const runnable = Effect.provide(program, AppLayer)
// Execute the program
const result = await Effect.runPromise(runnable)
Schema Validation
// src/schemas/order-schema.ts — runtime validation with Schema
import { Schema, Effect, pipe } from "effect"
// Define schema with precise types
const OrderItemSchema = Schema.Struct({
productId: Schema.String.pipe(Schema.minLength(1)),
productName: Schema.String.pipe(Schema.minLength(1)),
quantity: Schema.Number.pipe(Schema.positive(), Schema.int()),
priceCents: Schema.Number.pipe(Schema.positive(), Schema.int()),
})
const CreateOrderRequestSchema = Schema.Struct({
customerId: Schema.String.pipe(Schema.minLength(1)),
items: Schema.Array(OrderItemSchema).pipe(Schema.minItems(1)),
shippingAddress: Schema.Struct({
line1: Schema.String,
city: Schema.String,
country: Schema.String.pipe(Schema.length(2)),
postalCode: Schema.String,
}),
})
// Infer TypeScript type from schema
type CreateOrderRequest = Schema.Schema.Type<typeof CreateOrderRequestSchema>
// Decode unknown input with typed errors
const parseCreateOrderRequest = (input: unknown) =>
pipe(
Schema.decodeUnknown(CreateOrderRequestSchema)(input),
Effect.mapError((parseError) =>
new ValidationError({
field: "request",
message: String(parseError),
})
)
)
// API route handler with Effect
const handleCreateOrder = (rawBody: unknown) =>
Effect.gen(function* () {
const request = yield* parseCreateOrderRequest(rawBody)
const order = yield* createOrderEffect(request.customerId, request.items)
return order
})
Concurrent Effects
// src/concurrent/parallel.ts — Effect concurrency patterns
import { Effect, pipe, Fiber, Queue } from "effect"
// Process multiple orders in parallel with bounded concurrency
const processBatch = (orderIds: string[]) =>
Effect.forEach(
orderIds,
(id) => processOrder(id),
{ concurrency: 10 } // Max 10 concurrent
)
// Race: return first to succeed
const fetchFromAnyReplica = (orderId: string) =>
Effect.race(
getOrderFromPrimary(orderId),
getOrderFromReplica(orderId),
)
// Parallel with independent error handling
const fetchOrderAndCustomer = (orderId: string) =>
Effect.all({
order: getOrderById(orderId),
customer: getCustomerForOrder(orderId),
}, { concurrency: "unbounded" })
// Fibers for background tasks
const startBackgroundProcessing = Effect.gen(function* () {
const fiber = yield* Effect.fork(
Effect.forever(
pipe(
processNextPendingOrder(),
Effect.catchAll((error) => Effect.log(`Error: ${error._tag}`)),
Effect.delay("1 second"),
)
)
)
// Fiber can be interrupted later
return fiber
})
Error Recovery
// src/effects/recovery.ts — error handling strategies
import { Effect, pipe } from "effect"
// Catch specific error types
const getOrderWithFallback = (orderId: string) =>
pipe(
getOrderById(orderId),
Effect.catchTag("OrderNotFoundError", (_err) =>
Effect.succeed(null) // Return null instead of failing
),
Effect.catchTag("DatabaseError", (err) =>
pipe(
Effect.logError(`DB error: ${err.message}`),
Effect.andThen(getOrderFromReadReplica(orderId))
)
),
)
// Retry with backoff
const reliableDbCall = <A>(effect: Effect.Effect<A, DatabaseError>) =>
pipe(
effect,
Effect.retry({
times: 3,
schedule: Schedule.exponential("100 millis"),
}),
Effect.catchTag("DatabaseError", (err) =>
Effect.fail(new DatabaseError({
message: `Failed after 3 retries: ${err.message}`,
}))
),
)
For the Zod validation library that provides similar runtime type checking for TypeScript without Effect’s broader effect system, see the Zod guide for schema definitions and API validation. For the fp-ts functional programming alternative that implements the TypeScript HKT pattern Effect builds on, see the functional programming guide for type-safe composition. The Claude Skills 360 bundle includes Effect-TS skill sets covering typed errors, service layers, and Schema validation. Start with the free tier to try Effect program generation.