Expert guide for writing Effect-TS code, including project setup, core principles, data modeling with Schema, error handling, and the Context.Tag service pattern. Use when writing, refactoring, or analyzing TypeScript code using the Effect library.
Forbidden patterns, Effect.fn vs Effect.fnUntraced, resource management
references/testing-patterns.md
@effect/vitest with assert, TestClock, service mocking
references/quality-tooling-and-resources.md
Anti-patterns, validation checklist, packages
Core Principles
Effect is not just for async — Use Effect for any fallible operation
Immutability — Use Effect's immutable data structures (Data, Chunk, HashSet)
Type Safety — Track errors in types. No any or unknown in error channels
Composition — Build programs by composing small Effects
Schema-First — Define data models using Schema with branded types
Pattern Matching — Use Match for all branching over tagged unions (never switch/if-else on _tag)
Effect Data Types — Use Option (not null), Either (not ad-hoc), Duration (not raw ms), DateTime (not Date), BigDecimal (not floats), Redacted (for secrets). See references/data-types.md
Quick Reference
Import Convention
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as Match from "effect/Match";
import * as Option from "effect/Option";
import * as Either from "effect/Either";
import * as Data from "effect/Data";
import * as Duration from "effect/Duration";
import { pipe } from "effect/Function";
// Also: DateTime, BigDecimal, Chunk, HashSet, Exit, Cause, Redacted
Always use Match — Match.exhaustive catches missing cases at compile time. See references/pattern-matching.md.
// Match.type — reusable matcher function
const handle = Match.type<Status>().pipe(
Match.tag("Pending", (s) => `Pending since ${s.requestedAt}`),
Match.tag("Approved", (s) => `Approved by ${s.approvedBy}`),
Match.exhaustive // Compile error if any variant is missing
);
// Match.valueTags — shorthand for immediate matching
Match.valueTags(status, {
Pending: (s) => `Pending since ${s.requestedAt}`,
Approved: (s) => `Approved by ${s.approvedBy}`,
});
Service Pattern (Context.Tag)
See references/class-patterns.md for full pattern with factory methods and layers.
CRITICAL: Use assert from @effect/vitest for it.effect. Never expect with it.effect. See references/testing-patterns.md.
import { assert, describe, it } from "@effect/vitest";
it.effect("processes data", () =>
Effect.gen(function* () {
const result = yield* processData("input");
assert.strictEqual(result, "expected");
}).pipe(Effect.provide(MyService.testLayer))
);
Forbidden Patterns
// NEVER: try-catch in Effect.gen — use Effect.exit instead
Effect.gen(function* () {
try { yield* someEffect } catch (e) { } // WRONG — will never catch
});
// NEVER: Type assertions
const value = something as any; // FORBIDDEN
const value = something as never; // FORBIDDEN
// NEVER: Missing return on terminal yield
Effect.gen(function* () {
if (bad) { yield* Effect.fail("err") } // Missing return!
});
// NEVER: switch/if-else on _tag — use Match instead
switch (status._tag) { /* no exhaustiveness checking! */ }
// NEVER: Effect.runSync inside Effects
Effect.gen(function* () { Effect.runSync(sideEffect) }); // Loses error tracking
// NEVER: Native JS where Effect data types exist
const x: string | null = null; // Use Option<string>
const delay = 5000; // Use Duration.seconds(5)
const now = new Date(); // Use DateTime.now or DateTime.unsafeNow()
const price = 0.1 + 0.2; // Use BigDecimal for precision
const secret = "sk-1234"; // Use Redacted.make("sk-1234")
// NEVER: expect with it.effect
it.effect("test", () => Effect.gen(function* () {
expect(result).toBe(value) // WRONG — use assert.strictEqual
}));
// NEVER: Inline layers (breaks memoization)
Layer.provide(Postgres.layer({ url })) // Store in constant instead
Validation Checklist
Imports use import * as Module from "effect/Module"
Effect.gen for complex logic, pipe for linear, Effect.fn for public API
Match for all _tag branching with Match.exhaustive
Branded types for domain primitives (IDs, Emails)
Errors: Data.TaggedError (discrimination) or Schema.TaggedError (serializable)
No any/unknown in error channels, no type assertions
No try-catch in Effect.gen — use Effect.exit
return yield* for terminal effects (Effect.fail, Effect.interrupt)
Services: Context.Tag with static factory methods and Effect.fn tracing
Layers: Layer.merge/Layer.provide, parameterized layers in constants
Resources: Effect.acquireRelease or Effect.scoped
Option for nullable values, Either for sync success/failure
Duration for time values, DateTime for dates (not Date)
BigDecimal for financial/precise math, Redacted for secrets
Data.struct/Data.Class for structural equality, HashSet for sets
Clock.currentTimeMillis instead of Date.now()
Tests: assert from @effect/vitest (not expect) with it.effect
Run pnpm run typecheck and pnpm run test
Reference Implementation
See /packages/looper/src/data/api-client/api-client.ts for Context.Tag service pattern.
Effect Solutions CLI
pnpm exec effect-solutions list # List all topics
pnpm exec effect-solutions show <slug...> # Read topics
pnpm exec effect-solutions search <term> # Search by keyword
1---2name: effect-ts3description: Expert guide for writing Effect-TS code, including project setup, core principles, data modeling with Schema, error handling, and the Context.Tag service pattern. Use when writing, refactoring, or analyzing TypeScript code using the Effect library.4allowed-tools: Read, Grep, Glob5---67# Effect-TS Developer Guide89Guidelines, patterns, and best practices for Effect-TS in this project.1011## Reference Documents1213Read the relevant reference before writing code. `references/core-patterns.md` is the master index.1415| Reference | Topics |16|---|---|17| `references/foundations.md` | Setup, imports, TypeScript config |18| `references/construction-and-style.md` | `Effect.gen`, `pipe`, `Effect.fn`, `Effect.fnUntraced` |19| `references/schema-errors-config.md` | Schema modeling, errors, config, retry |20| `references/pattern-matching.md` | **`Match.type`, `Match.value`, `Match.tag`, `Match.exhaustive`** — mandatory for tagged unions |21| `references/control-flow-and-runtime.md` | `Effect.if`, `Effect.when`, loops, `runSync`, `runPromise`, `ManagedRuntime` |22| `references/data-types.md` | **All data types**: `Option`, `Either`, `Data`, `Exit`, `Cause`, `Duration`, `DateTime`, `BigDecimal`, `Chunk`, `HashSet`, `Redacted` |23| `references/data-and-testing.md` | Option/Either/Array quick ref, `@effect/vitest` setup |24| `references/concurrency-and-resources.md` | Concurrency, `Scope`, finalizers, resources |25| `references/streams-deep-dive.md` | Creating, operations, grouping, partitioning, broadcasting, buffering, throttling, error handling |26| `references/sink.md` | Sink constructors, collecting, folding, operations, concurrency, leftovers, `Stream.transduce` |27| `references/batching-and-caching.md` | Request batching (`RequestResolver`), `cachedWithTTL` |28| `references/schema-transforms-and-filters.md` | `Schema.transform`, `Schema.filter`, refinements |29| `references/api-platform-observability.md` | `HttpApi`, logging, tracing, spans |30| `references/class-patterns.md` | `Context.Tag` service pattern, layers, memoization, testing |31| `references/error-handling-patterns.md` | `Data.TaggedError`, `Schema.TaggedError`, error composition, recovery |32| `references/library-development-patterns.md` | Forbidden patterns, `Effect.fn` vs `Effect.fnUntraced`, resource management |33| `references/testing-patterns.md` | `@effect/vitest` with `assert`, `TestClock`, service mocking |34| `references/quality-tooling-and-resources.md` | Anti-patterns, validation checklist, packages |3536## Core Principles37381. **Effect is not just for async** — Use `Effect` for any fallible operation392. **Immutability** — Use Effect's immutable data structures (`Data`, `Chunk`, `HashSet`)403. **Type Safety** — Track errors in types. No `any` or `unknown` in error channels414. **Composition** — Build programs by composing small Effects425. **Schema-First** — Define data models using `Schema` with branded types436. **Pattern Matching** — Use `Match` for all branching over tagged unions (never `switch`/`if-else` on `_tag`)447. **Effect Data Types** — Use `Option` (not null), `Either` (not ad-hoc), `Duration` (not raw ms), `DateTime` (not Date), `BigDecimal` (not floats), `Redacted` (for secrets). See `references/data-types.md`4546## Quick Reference4748### Import Convention4950```typescript51import * as Context from "effect/Context";52import * as Effect from "effect/Effect";53import * as Layer from "effect/Layer";54import * as Schema from "effect/Schema";55import * as Match from "effect/Match";56import * as Option from "effect/Option";57import * as Either from "effect/Either";58import * as Data from "effect/Data";59import * as Duration from "effect/Duration";60import { pipe } from "effect/Function";61// Also: DateTime, BigDecimal, Chunk, HashSet, Exit, Cause, Redacted62```6364### Effect.gen vs pipe vs Effect.fn6566```typescript67// Effect.gen — complex logic with branching68Effect.gen(function* () {69 const user = yield* fetchUser(id);70 if (user.isAdmin) yield* logAdminAccess(user);71 return user;72});7374// pipe — linear transformations75pipe(fetchData(), Effect.map(transform), Effect.flatMap(save));7677// Effect.fn — traced reusable functions (public API)78const processUser = Effect.fn("processUser")(function* (userId: string) {79 const user = yield* getUser(userId);80 return yield* processData(user);81});82```8384### Error Handling8586`Data.TaggedError` for in-process discrimination, `Schema.TaggedError` for serializable errors. See `references/error-handling-patterns.md`.8788```typescript89export class NotFoundError extends Data.TaggedError("NotFoundError")<{90 id: string;91}> {}9293// Recovery94pipe(riskyOp, Effect.catchTag("NotFoundError", (e) => Effect.succeed(null)));95```9697### Pattern Matching (Mandatory for Tagged Unions)9899**Always use `Match`** — `Match.exhaustive` catches missing cases at compile time. See `references/pattern-matching.md`.100101```typescript102// Match.type — reusable matcher function103const handle = Match.type<Status>().pipe(104 Match.tag("Pending", (s) => `Pending since ${s.requestedAt}`),105 Match.tag("Approved", (s) => `Approved by ${s.approvedBy}`),106 Match.exhaustive // Compile error if any variant is missing107);108109// Match.valueTags — shorthand for immediate matching110Match.valueTags(status, {111 Pending: (s) => `Pending since ${s.requestedAt}`,112 Approved: (s) => `Approved by ${s.approvedBy}`,113});114```115116### Service Pattern (Context.Tag)117118See `references/class-patterns.md` for full pattern with factory methods and layers.119120```typescript121export class MyService extends Context.Tag("@myapp/MyService")<122 MyService,123 { readonly find: (id: string) => Effect.Effect<Result, NotFoundError> }124>() {125 static readonly layer = Layer.effect(MyService, Effect.gen(function* () {126 const db = yield* Database;127 return MyService.of({ find: MyService.createFind(db) });128 }));129}130```131132### Testing133134**CRITICAL**: Use `assert` from `@effect/vitest` for `it.effect`. Never `expect` with `it.effect`. See `references/testing-patterns.md`.135136```typescript137import { assert, describe, it } from "@effect/vitest";138139it.effect("processes data", () =>140 Effect.gen(function* () {141 const result = yield* processData("input");142 assert.strictEqual(result, "expected");143 }).pipe(Effect.provide(MyService.testLayer))144);145```146147## Forbidden Patterns148149```typescript150// NEVER: try-catch in Effect.gen — use Effect.exit instead151Effect.gen(function* () {152 try { yield* someEffect } catch (e) { } // WRONG — will never catch153});154155// NEVER: Type assertions156const value = something as any; // FORBIDDEN157const value = something as never; // FORBIDDEN158159// NEVER: Missing return on terminal yield160Effect.gen(function* () {161 if (bad) { yield* Effect.fail("err") } // Missing return!162});163164// NEVER: switch/if-else on _tag — use Match instead165switch (status._tag) { /* no exhaustiveness checking! */ }166167// NEVER: Effect.runSync inside Effects168Effect.gen(function* () { Effect.runSync(sideEffect) }); // Loses error tracking169170// NEVER: Native JS where Effect data types exist171const x: string | null = null; // Use Option<string>172const delay = 5000; // Use Duration.seconds(5)173const now = new Date(); // Use DateTime.now or DateTime.unsafeNow()174const price = 0.1 + 0.2; // Use BigDecimal for precision175const secret = "sk-1234"; // Use Redacted.make("sk-1234")176177// NEVER: expect with it.effect178it.effect("test", () => Effect.gen(function* () {179 expect(result).toBe(value) // WRONG — use assert.strictEqual180}));181182// NEVER: Inline layers (breaks memoization)183Layer.provide(Postgres.layer({ url })) // Store in constant instead184```185186## Validation Checklist187188- [ ] Imports use `import * as Module from "effect/Module"`189- [ ] `Effect.gen` for complex logic, `pipe` for linear, `Effect.fn` for public API190- [ ] `Match` for all `_tag` branching with `Match.exhaustive`191- [ ] Branded types for domain primitives (IDs, Emails)192- [ ] Errors: `Data.TaggedError` (discrimination) or `Schema.TaggedError` (serializable)193- [ ] No `any`/`unknown` in error channels, no type assertions194- [ ] No try-catch in `Effect.gen` — use `Effect.exit`195- [ ] `return yield*` for terminal effects (`Effect.fail`, `Effect.interrupt`)196- [ ] Services: `Context.Tag` with static factory methods and `Effect.fn` tracing197- [ ] Layers: `Layer.merge`/`Layer.provide`, parameterized layers in constants198- [ ] Resources: `Effect.acquireRelease` or `Effect.scoped`199- [ ] `Option` for nullable values, `Either` for sync success/failure200- [ ] `Duration` for time values, `DateTime` for dates (not `Date`)201- [ ] `BigDecimal` for financial/precise math, `Redacted` for secrets202- [ ] `Data.struct`/`Data.Class` for structural equality, `HashSet` for sets203- [ ] `Clock.currentTimeMillis` instead of `Date.now()`204- [ ] Tests: `assert` from `@effect/vitest` (not `expect`) with `it.effect`205- [ ] Run `pnpm run typecheck` and `pnpm run test`206207## Reference Implementation208209See `/packages/looper/src/data/api-client/api-client.ts` for Context.Tag service pattern.210211## Effect Solutions CLI212213```bash214pnpm exec effect-solutions list # List all topics215pnpm exec effect-solutions show <slug...> # Read topics216pnpm exec effect-solutions search <term> # Search by keyword217```
Run npx skillmds@latest add pedronauck/effect-ts in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Expert guide for writing Effect-TS code, including project setup, core principles, data modeling with Schema, error handling, and the Context.Tag service pattern. Use when writing, refactoring, or analyzing TypeScript code using the Effect library. It is listed under Coding & Dev Tools on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Independent scanners report: SkillSpector: PASS, Skill Scanner: PASS. Capability flags: reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
pedronauck (@pedronauck) published this skill. Their other Agent Skills are listed on their SkillMD profile.