| name | effect-ts |
| description | Comprehensive guide for Effect-TS, the functional TypeScript library. Use when building Effect applications, especially MCP servers. Covers correct APIs, common misconceptions, and MCP-specific patterns. |
Effect-TS Expert Guide
Effect-TS is a functional TypeScript library providing typed effects, structured concurrency, and a robust runtime. This skill covers correct usage patterns and addresses common misconceptions from LLM-generated content.
Quick Reference
import { Effect, Layer, Context, Fiber, Schedule, Cache, Scope } from 'effect';
import { Schema, JSONSchema } from '@effect/schema';
Core Type Signature:
Effect<Success, Error, Requirements>;
Common Misconceptions
LLM outputs often contain incorrect APIs. Use this table to correct them:
| Wrong (common in AI outputs) | Correct |
|---|
Effect.cachedWithTTL(...) | Cache.make({ timeToLive: Duration }) |
Effect.cachedInvalidateWithTTL(...) | cache.invalidate(key) / cache.invalidateAll() |
Effect.match(...) | Effect.either + Either.match, or Effect.catchTag |
| "thread-local storage" | "fiber-local storage" via FiberRef |
| JSON Schema Draft 2020-12 | @effect/schema generates Draft-07 |
| fibers are "cancelled" | fibers are "terminated" or "interrupted" |
| all queues have back-pressure | only bounded queues; sliding/dropping do not |
--only=production | --omit=dev (npm 7+) |
Error Handling: Two Error Types
Effect distinguishes between:
- Expected Errors (Failures) - Typed in
E channel, must be handled
- Unexpected Errors (Defects) - Runtime bugs, captured but not typed
const fetchUser = (id: string): Effect.Effect<User, UserNotFoundError | NetworkError> => ...
const handled = pipe(
fetchUser("123"),
Effect.catchTag("UserNotFoundError", (e) => Effect.succeed(defaultUser)),
Effect.catchTag("NetworkError", (e) => Effect.retry(schedule))
);
Effect.catchAllDefect(program, (defect) =>
Console.error("Unexpected error", defect)
);
Fibers & Cancellation (Critical for MCP)
Fibers are lightweight virtual threads with native interruption:
const fiber = yield * Effect.fork(longRunningTask);
yield * Fiber.interrupt(fiber);
const parent = Effect.gen(function* () {
yield* Effect.fork(backgroundTask);
yield* mainTask;
});
yield * Effect.forkDaemon(longLivedBackgroundTask);
Concurrency Primitives
Effect.race - First Wins, Losers Interrupted
const result = yield * Effect.race(fetchFromCache, fetchFromDatabase);
Effect.all with Concurrency Control
const results =
yield *
Effect.all(documents.map(processDoc), {
concurrency: 5,
});
Queue Types
const bounded = yield * Queue.bounded<string>(100);
const dropping = yield * Queue.dropping<string>(100);
const sliding = yield * Queue.sliding<string>(100);
Layers for Dependency Injection
Layers construct services without leaking dependencies:
class Database extends Context.Tag('Database')<
Database,
{ query: (sql: string) => Effect.Effect<Result> }
>() {}
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config;
return {
query: sql => Effect.tryPromise(() => runQuery(sql, config)),
};
}),
);
const runnable = program.pipe(Effect.provide(DatabaseLive));
const DatabaseTest = Layer.succeed(Database, {
query: () => Effect.succeed(mockResult),
});
Resource Management
Effect.ensuring - Always Runs Finalizer
const program = pipe(
Effect.tryPromise(() => openConnection()),
Effect.ensuring(Console.log('Cleanup')),
);
acquireUseRelease Pattern
const withConnection = Effect.acquireUseRelease(
Effect.tryPromise(() => db.connect()),
conn => Effect.tryPromise(() => conn.query('SELECT *')),
conn => Effect.promise(() => conn.close()),
);
Scope for Resource Lifecycle
Effect.scoped(
Effect.gen(function* () {
const file = yield* openFile('data.txt');
const data = yield* file.read();
return data;
}),
);
Caching
There is no Effect.cachedWithTTL. Use the Cache module:
import { Cache } from 'effect';
const cache =
yield *
Cache.make({
capacity: 100,
timeToLive: Duration.minutes(5),
lookup: (key: string) => fetchExpensiveData(key),
});
const value = yield * cache.get('my-key');
yield * cache.invalidate('my-key');
yield * cache.invalidateAll();
Retry with Schedule
import { Schedule } from 'effect';
const policy = Schedule.exponential('100 millis').pipe(
Schedule.intersect(Schedule.recurs(3)),
);
const robust = Effect.retry(unstableOperation, policy);
const untilSuccess = Effect.retry(operation, {
until: err => err.code === 'RATE_LIMITED',
});
Schema & JSON Schema
@effect/schema generates JSON Schema Draft-07 (not 2020-12):
import { Schema, JSONSchema } from '@effect/schema';
const User = Schema.Struct({
id: Schema.String,
age: Schema.Number.pipe(Schema.positive()),
});
const jsonSchema = JSONSchema.make(User);
const user = Schema.decodeUnknownSync(User)(rawData);
const json = Schema.encodeSync(User)(user);
Observability & OpenTelemetry
Effect has native OpenTelemetry integration:
import { NodeSdk } from '@effect/opentelemetry';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
const traced = Effect.withSpan('processRequest')(myEffect);
yield * Effect.log('Processing request');
yield * Effect.annotateLogs('requestId', 'abc-123');
const RequestId = FiberRef.unsafeMake<string>('');
yield * FiberRef.set(RequestId, 'req-456');
When NOT to Use Effect
| Scenario | Recommendation |
|---|
| Simple MCP tool (< 100 LOC) | Use FastMCP or vanilla SDK |
| Team unfamiliar with FP | Steep learning curve; consider NestJS |
| Bundle size critical | Effect adds 15-25kb gzipped minimum |
| Existing NestJS/TypeORM codebase | Impedance mismatch with class-based DI |
MCP Server Patterns
Tool Handler with Typed Errors
const searchTool = Effect.gen(function* () {
const args = yield* parseArgs(input);
const db = yield* Database;
const results = yield* db.query(args.query);
return formatResults(results);
}).pipe(
Effect.catchTag('ParseError', () =>
Effect.fail({ code: -32602, message: 'Invalid params' }),
),
Effect.catchTag('DatabaseError', () =>
Effect.fail({ code: -32603, message: 'Internal error' }),
),
);
Request Scoping
const handleRequest = (request: MCPRequest) =>
Effect.scoped(
Effect.gen(function* () {
const tempFile = yield* createTempFile();
const result = yield* processRequest(request, tempFile);
return result;
}),
);
Resources