
Effect Ts
- 2.6k installs
- 68 repo stars
- Updated July 30, 2026
- paulrberg/agent-skills
effect-ts is an expert skill for Effect library services, layers, Schema, Config, and tested runtime patterns.
About
Effect-TS Expert guides nontrivial Effect library work including services, layers, typed errors, Schema, Config, runtime concurrency, vitest, ai, sql, and effect-next integrations. Fast path inspects local project patterns first, reads critical-rules.md before edits, opens only matching reference files, and runs the narrowest proving test. Upstream source at ~/.effect is required for complex API or changelog questions; missing checkout must be cloned from Effect-TS/effect before proceeding. Research priority ranks codebase patterns, package source under packages/, and changelogs when behavior shifted recently. High-priority research triggers include services, layers, multi-error hierarchies, streams, resource scopes, and concurrent operations. Reference routing maps topics to dedicated files for services, schema, config, testing, platform, sql, ai, and migration from promises. Version baseline notes effect 3.x and matching @effect/* minor lines with drift acceptable within major boundaries. The skill avoids invocation for trivial imports and focuses when Effect semantics materially affect the change.
- Fast path: local patterns, critical-rules.md, targeted references, narrow tests.
- Clone ~/.effect from github.com/Effect-TS/effect when upstream detail is required.
- Research priority: codebase, package source, then changelogs.
- Routes Schema, Config, Layer, Stream, and SQL topics to reference files.
- Matches @effect package minor lines when checking upstream compatibility.
Effect Ts by the numbers
- 2,592 all-time installs (skills.sh)
- +79 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #201 of 4,353 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
effect-ts capabilities & compatibility
- Capabilities
- critical rules enforcement before edits · reference routing by effect topic · upstream ~/.effect research protocol · local pattern consistency checks · testing guidance for @effect/vitest
- Use cases
- api development · testing · refactoring
- Runs
- Runs locally
- Pricing
- Free
npx skills add https://github.com/paulrberg/agent-skills --skill effect-tsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 68 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 30, 2026 |
| Repository | paulrberg/agent-skills ↗ |
How do I implement this feature correctly with Effect services, layers, and typed errors?
Implement Effect-TS services, layers, Schema, Config, streams, and tests following project and upstream reference patterns.
Who is it for?
Nontrivial Effect-TS refactors, new services, Schema, or sql and ai package work.
Skip if: Skip for files that merely import effect without semantic Effect changes.
When should I use this skill?
User works on Effect Layer, Schema, Config, streams, or @effect/vitest tests.
What you get
Code following local Effect conventions with critical rules applied and targeted tests passing.
- Typed Config effect definitions
- Redacted secret handlers
- ConfigProvider Layer setup
Files
Effect-TS Expert
Expert guidance for functional programming with the Effect library, covering error handling, dependency injection, composability, testing, and runtime-boundary patterns.
Fast Path
Use this skill for nontrivial Effect work. Do not route through this skill just because a file imports from effect; use it when the change depends on Effect semantics such as services, layers, typed errors, Schema, Config, runtime/concurrency, streams, or Effect-aware tests.
For small code edits:
1. Inspect local project patterns first. 2. Read ./references/critical-rules.md before writing or changing Effect code. 3. Open only the reference files that match the task. 4. Run the narrowest project check that proves the changed Effect behavior.
Upstream Source Check
Check the Effect source at ~/.effect only when the task needs upstream API details, changelog verification, or a complex type/runtime question that local project patterns do not answer.
If ~/.effect is required but missing, stop and inform the user. Clone it before proceeding:
git clone https://github.com/Effect-TS/effect.git ~/.effectUpstream Baseline
Last checked against ~/.effect HEAD 05d72eab7 from 2026-06-05:
effect@3.21.3@effect/ai@0.36.0@effect/ai-openai@0.40.0@effect/platform@0.96.1@effect/sql@0.51.1@effect/rpc@0.75.1@effect/cluster@0.59.0
Your local ~/.effect checkout need not match these exact versions. Drift is expected and fine as long as the major versions match. For effect that means the 3.x line. For the 0.x @effect/* packages, semver treats the leading non-zero segment as the break boundary, so match the minor too (e.g. @effect/ai@0.36.x). Patch differences, and minor differences on stable packages, won't invalidate this skill's guidance; only a break-boundary bump warrants caution.
Local ~/.effect drift is usually fine for routine project work. If git -C ~/.effect log -1 --oneline is newer and the task depends on upstream behavior, inspect the touched package changelogs and commits before relying on this skill. Capture public API or guidance changes in a reference file.
Research Strategy
Effect-TS has many ways to accomplish the same task. For moderate to high complexity tasks, research enough to choose the least surprising pattern that fits the current codebase. Prefer parallel local reads/searches. Use subagents only when the environment explicitly supports them and the task has separable research tracks.
Research Sources (Priority Order)
1. Codebase Patterns First — Examine similar patterns in the current project before implementing. If Effect patterns exist in the codebase, follow them for consistency. If no patterns exist, skip this step.
2. Effect Source Code — For complex type errors, unclear behavior, or implementation details, examine the relevant package source under ~/.effect/packages/<package>/src/. For core Effect, use ~/.effect/packages/effect/src/.
3. Package Changelogs — When behavior changed recently, read the relevant changelog under ~/.effect/packages/*/ before inferring from old examples.
When to Research
HIGH Priority (Always Research):
- Implementing Services, Layers, or complex dependency injection
- Error handling with multiple error types or complex error hierarchies
- Stream-based operations and reactive patterns
- Resource management with scoped effects and cleanup
- Concurrent/parallel operations and performance-critical code
- Testing patterns, especially unfamiliar test scenarios
MEDIUM Priority (Research if Complex):
- Refactoring imperative code (try-catch, promises) to Effect patterns
- Adding new service dependencies or restructuring service layers
- Custom error types or extending existing error hierarchies
- Integrations with external systems (databases, APIs, third-party services)
Research Approach
- Focus on canonical, readable, and maintainable solutions rather than clever optimizations
- Verify suggested approaches against existing codebase patterns for consistency (if patterns exist)
- When multiple approaches are possible, prefer the one already used locally unless it is clearly flawed
Reference Routing
Open references selectively:
| Task shape | Read |
|---|---|
| Writing/changing Effect code | ./references/critical-rules.md |
Services, Layers, Effect.Service, Context.Tag, Effect.fn | ./references/services-layers.md |
| Config, env vars, secrets, custom providers | ./references/config.md |
| Schema decoding, JSON Schema, AI parameter shapes | ./references/schema-jsonschema.md |
@effect/vitest, TestClock, sleeps/retries, fibers in tests | ./references/testing.md |
Resources, scheduling, refs, concurrency, SubscriptionRef | ./references/runtime.md |
| Streams, backpressure, bounded consumption | ./references/streams.md |
Pattern matching, tagged unions, Data.taggedEnum | ./references/pattern-matching.md |
@effect/ai tools/providers/OpenAI integration | ./references/ai.md |
@effect/sql, SqlSchema, repository row decoding | ./references/sql.md |
@effect/platform, @effect/rpc, deployment runtimes | ./references/platform-rpc.md |
@prb/effect-next / Next.js App Router | ./references/next-js.md |
@effect-atom/* React state | ./references/effect-atom.md |
| Array/Record reducers, filters, predicates, sorting | ./references/collection-operations.md |
| Tiny utility functions, deprecations | ./references/quick-utils.md |
| Upstream drift or recent package behavior | ./references/recent-upstream.md |
Codebase Pattern Discovery
When working in a project that uses Effect, check for existing patterns before implementing new code:
1. Search for Effect imports — Look for files importing from 'effect' to understand existing usage 2. Identify service patterns — Find how Services and Layers are structured in the project 3. Note error handling conventions — Check how errors are defined and propagated 4. Examine test patterns — Look at how Effect code is tested in the project
If no Effect patterns exist in the codebase, proceed using canonical patterns from the Effect source and examples. Do not block on missing codebase patterns.
Effect Principles
Apply these core principles when writing Effect code:
Error Handling
- Use Effect's typed error system instead of throwing exceptions
- Prefer
Schema.TaggedErrorfor domain/API errors that cross serialization or HTTP boundaries - Use
Data.TaggedErrorfor internal, non-encoded errors when Schema integration is unnecessary - Use
Effect.fail,Effect.catchTag,Effect.catchAllfor error control flow - See
./references/critical-rules.mdfor forbidden patterns
Dependency Injection
- Implement dependency injection using Services and Layers
- Define services with
Context.Tag - Compose layers with
Layer.merge,Layer.provide - Use
Effect.provideto inject dependencies
Composability
- Leverage Effect's composability for complex operations
- Use appropriate constructors:
Effect.succeed,Effect.fail,Effect.tryPromise,Effect.try - Apply proper resource management with scoped effects
- Chain operations with
Effect.flatMap,Effect.map,Effect.tap
Code Quality
- Write type-safe code that leverages Effect's type system
- Prefer
Schema.Classfor domain and API models that need construction, validation, encoding, or equality - Use
Effect.genfor readable sequential code - Implement proper testing patterns using Effect's testing utilities
- Prefer
Effect.fn()for automatic telemetry and better stack traces
Boundary Refactors
- Use Effect services at IO/runtime boundaries where dependency injection, testability, or resource safety improves the
design.
- Do not make pure helpers, module constants, path strings, or tiny build-time utilities effectful just to replace Node or
platform APIs.
@effect/platformservices such asFileSystemandPathare environment requirements. Keep them inside existing
service/runtime boundaries unless widening a function's environment is a deliberate design improvement.
- Preserve local domain facades when they already centralize Effect services, for example filesystem, reporter, logger, or
config services.
Critical Rules
Read ./references/critical-rules.md before writing or changing nontrivial Effect code. Key guidelines:
- INEFFECTIVE: try-catch in Effect.gen (Effect failures aren't thrown)
- AVOID: Type assertions (as never/any/unknown)
- RECOMMENDED:
return yield*pattern for errors (makes termination explicit)
Common Failure Modes
Quick links to patterns that frequently cause issues:
- SubscriptionRef version mismatch —
unsafeMake is not a function→ runtime.md - Cancellation vs Failure — Interrupts aren't errors → Error Taxonomy
- Option vs null — Use Option internally, null at boundaries → option-null.md
- Stream backpressure — Infinite streams hang → streams.md
- JSON Schema closed records —
Schema.Record(String, Never)emits no extra properties →
schema-jsonschema.md
- No-parameter AI tools — Use
Tool.EmptyParamsor omitparameters→ ai.md - Layer reuse surprises — Layers memoize by object identity; use
Layer.freshonly when needed →
services-layers.md
Explaining Solutions
When providing solutions, explain the Effect-TS concepts being used and why they're appropriate for the specific use case. If encountering patterns not covered in the documentation, suggest improvements while maintaining consistency with existing codebase patterns (when they exist).
Quick Reference
Creating Effects
Effect.succeed(value) // Wrap success value
Effect.fail(error) // Create failed effect
Effect.tryPromise(fn) // Wrap promise-returning function
Effect.try(fn) // Wrap synchronous throwing function
Effect.sync(fn) // Wrap synchronous non-throwing functionComposing Effects
Effect.flatMap(effect, fn) // Chain effects
Effect.map(effect, fn) // Transform success value
Effect.tap(effect, fn) // Side effect without changing value
Effect.all([...effects]) // Run effects (concurrency configurable)
Effect.forEach(items, fn) // Map over items with effects
// Collect ALL errors (not just first)
Effect.all([e1, e2, e3], { mode: "validate" }) // Returns all failures
// Partial success handling
Effect.partition([e1, e2, e3]) // Returns [failures, successes]Error Handling
// Domain/API errors that cross boundaries: prefer Schema.TaggedError
class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
"UserNotFoundError",
{
userId: Schema.String
}
) {
get message() {
return `User not found: ${this.userId}`
}
}
// Internal-only errors may use Data.TaggedError
class CacheMissError extends Data.TaggedError("CacheMissError")<{
userId: string
}> {}
// Direct yield of errors (no Effect.fail wrapper needed)
Effect.gen(function* () {
if (!user) {
return yield* new UserNotFoundError({ userId })
}
})
Effect.catchTag(effect, tag, fn) // Handle specific error tag
Effect.catchAll(effect, fn) // Handle all errors
Effect.result(effect) // Convert to Exit value
Effect.orElse(effect, alt) // Fallback effectError Taxonomy
Categorize errors for appropriate handling:
| Category | Examples | Handling |
|---|---|---|
| Expected Rejections | User cancel, deny | Graceful exit, no retry |
| Domain Errors | Validation, business rules | Show to user, don't retry |
| Defects | Bugs, assertions | Log + alert, investigate |
| Interruptions | Fiber cancel, timeout | Cleanup, may retry |
| Unknown/Foreign | Thrown exceptions | Normalize at boundary |
// Pattern: Normalize unknown errors at boundary
const safeBoundary = Effect.catchAllDefect(effect, (defect) =>
Effect.fail(new UnknownError({ cause: defect }))
)
// Pattern: Catch user-initiated cancellations separately
Effect.catchTag(effect, "UserCancelledError", () => Effect.succeed(null))
// Pattern: Handle interruptions differently from failures
Effect.onInterrupt(effect, () => Effect.log("Operation cancelled"))Pattern Matching (Match Module)
When you need to use Effect's Match module for pattern matching, see references/pattern-matching.md.
Schema and JSON Schema
For schema decoding, JSON Schema generation, closed object shapes, and Schema.Record({ key: Schema.String, value: Schema.Never }), see references/schema-jsonschema.md.
AI Tooling
For @effect/ai tool definitions, empty tool parameters, OpenAI strict schema behavior, and prompt cache enum gotchas, see references/ai.md.
Services and Layers / Generator Pattern
For service definition patterns (Context.Tag, Effect.Service, Context.Reference, Context.ReadonlyTag) and the generator pattern (Effect.gen, Effect.fn), see references/services-layers.md.
Runtime Patterns (Resource Management, Duration, Scheduling, State, SubscriptionRef, Concurrency)
For resource lifecycles, durations, scheduling, state management, reactive refs, and concurrency primitives, see references/runtime.md.
Configuration & Environment Variables
When you need to read configuration with Config, handle secrets via Redacted, or wire custom config providers, see references/config.md.
Collection Operations (Array, Record, Order)
For Effect's Array/Record reducers, filters, predicates, object traversal replacements, and Order sorting helpers, see references/collection-operations.md.
Quick Utilities (Functions, Deprecations)
For small utility functions like constVoid and the running list of deprecations, see references/quick-utils.md.
Platform and RPC
For HttpLayerRouter, RpcSerialization.makeMsgPack, and deployment gotchas such as Cloudflare Workers msgpack support, see references/platform-rpc.md.
Additional Resources
Local Effect Resources
- `~/.effect/packages/effect/src/` — Core Effect modules and implementation
External Resources
- Effect-Atom — https://github.com/tim-smart/effect-atom (open in browser for reactive state management patterns)
Reference Files
- `./references/ai.md` —
@effect/aitools,Tool.EmptyParams, OpenAI provider notes - `./references/config.md` —
Config,Redacted, and custom config providers - `./references/collection-operations.md` —
Array/Recordreducers, filters, predicates, sorting - `./references/critical-rules.md` — Forbidden patterns and mandatory conventions
- `./references/effect-atom.md` — Effect-Atom reactive state management for React
- `./references/next-js.md` — Effect + Next.js 15+ App Router integration patterns
- `./references/option-null.md` — Option vs null boundary patterns
- `./references/pattern-matching.md` —
Matchmodule for tagged unions and conditionals - `./references/platform-rpc.md` —
@effect/platformand@effect/rpcintegration notes - `./references/quick-utils.md` — Utility helpers and deprecations
- `./references/recent-upstream.md` — Recent upstream public changes reflected by this skill
- `./references/runtime.md` — Resource management, Duration, Scheduling, State, SubscriptionRef, Concurrency
- `./references/schema-jsonschema.md` — Schema decoding and JSON Schema generation patterns
- `./references/services-layers.md` — Services, Layers, generator (
Effect.gen/Effect.fn) - `./references/sql.md` —
@effect/sql,SqlSchema, row decoding, repository patterns - `./references/streams.md` — Stream patterns and backpressure gotchas
- `./references/testing.md` — Vitest deterministic testing patterns
policy:
allow_implicit_invocation: true
Effect AI
When to read: pull this in when defining @effect/ai tools, toolkits, provider-defined tools, or OpenAI languagemodel configuration.
Tool Parameters
@effect/ai@0.36.0 supports no-parameter tools directly. Omit parameters for the default empty parameter schema, or use Tool.EmptyParams when the emptiness needs to be explicit.
import { Tool } from "@effect/ai"
import { Schema } from "effect"
const GetCurrentTime = Tool.make("GetCurrentTime", {
description: "Returns the current timestamp",
success: Schema.Number
})
const Ping = Tool.make("Ping", {
parameters: Tool.EmptyParams,
success: Schema.String
})
const ReadFile = Tool.make("ReadFile").setParameters({
filePath: Schema.String
})
const NoArgsAgain = ReadFile.setParameters(Tool.EmptyParams)Tool.EmptyParams is Schema.Record({ key: Schema.String, value: Schema.Never }), so generated JSON Schema should be a closed empty object shape. Do not replace it with a loose Record<string, unknown>.
Tool Type Extraction
Use the built-in utility types when handlers need the decoded or encoded parameter shape:
type Params = Tool.Parameters<typeof ReadFile>
type EncodedParams = Tool.ParametersEncoded<typeof ReadFile>
type ParamsSchema = Tool.ParametersSchema<typeof ReadFile>OpenAI Strict Mode
@effect/ai-openai exposes strict?: boolean on OpenAiLanguageModel config.
- Default is strict structured-output behavior for generated tool schemas and JSON Schema response formats.
- Set
strict: falseonly when a model or schema construct cannot satisfy OpenAI strict schema requirements. - Recent versions consume
strictduring schema preparation; it should not be sent as a top-level Responses API request
parameter.
OpenAI Prompt Cache Enums
Use "in_memory" for prompt cache retention enum values. Older examples that use "in-memory" are stale.
const promptCacheRetention = "in_memory"Response Output Handling
Recent OpenAI provider versions deduplicate response.output items before JSON concatenation. If you see duplicated structured output in a pinned project, check @effect/ai-openai before working around it at the application layer.
Collection Operations (Array, Record, Order)
When to read: pull this in when replacing ad hoc reducers, filters, predicates, object traversal, or sorting with Effect
collection helpers.
Array Reducers and Predicates
Use Array as Arr when the input is already an iterable collection, or after you have intentionally converted a record with Record.values or Record.toEntries.
import { Array as Arr, Option } from "effect"
const totalCount = Arr.reduce(items, 0, (sum, item) => sum + item.count)
const activeItems = Arr.filter(items, (item) => item.active)
const hasFailures = Arr.some(items, (item) => item.status === "failed")
const allReady = Arr.every(items, (item) => item.status === "ready")
const firstAdmin = Arr.findFirst(users, (user) => user.role === "admin")
if (Option.isSome(firstAdmin)) {
firstAdmin.value.id
}
const [disabledItems, enabledItems] = Arr.partition(items, (item) => item.enabled)Object and Record Traversal
Arr does not enumerate object properties. Use Effect's Record module when keys matter or the result should remain record-shaped; use Record.values or Record.toEntries when you deliberately want array-shaped traversal.
import { Array as Arr, Record } from "effect"
// Replaces Object.values(record).reduce(...)
const totalCount = Record.reduce(countsById, 0, (sum, count) => sum + count)
// Replaces Object.entries(record).map(...) when key and value both matter
const labels = Record.collect(usersById, (id, user) => `${id}:${user.name}`)
// Use Arr after Record.values / Record.toEntries when array operators are clearer
const positiveCounts = Arr.filter(Record.values(countsById), (count) => count > 0)
const adminLabels = Arr.filter(Record.toEntries(usersById), ([id, user]) =>
id.startsWith("admin-") && user.role === "admin"
)
// Prefer Record.* when preserving record shape or key-aware traversal
const enabledById = Record.filter(usersById, (user) => user.enabled)
const namesById = Record.map(usersById, (user) => user.name)
const activeCount = Record.reduce(usersById, 0, (sum, user) => sum + (user.active ? 1 : 0))
const hasAdmin = Record.some(usersById, (user, id) => id.startsWith("admin-") && user.role === "admin")
const allEnabled = Record.every(usersById, (user) => user.enabled)Sorting
Use Order with Arr.sort, Arr.sortWith, or Arr.sortBy instead of copied mutable sorts like [...items].sort(...). These helpers accept any iterable and return a new array.
import { Array as Arr, Order } from "effect"
Arr.sort([3, 1, 2], Order.number) // [1, 2, 3]
Arr.sort(["b", "a", "c"], Order.string) // ["a", "b", "c"]
Arr.sort(new Set([3n, 1n, 2n]), Order.bigint) // [1n, 2n, 3n]
Arr.sortWith(users, (user) => user.age, Order.number)
Arr.sortBy(
users,
Order.mapInput(Order.number, (user: User) => user.age),
Order.mapInput(Order.string, (user: User) => user.name)
)
// Built-in orderings: Order.string, Order.number, Order.bigint, Order.boolean, Order.Date
// Reverse ordering: Order.reverse(Order.number)Replacement Cheatsheet
| Existing logic | Prefer |
|---|---|
array.reduce(...) | Arr.reduce(array, zero, f) |
array.filter(...) | Arr.filter(array, predicate) |
array.some(...) / array.every(...) | Arr.some(array, predicate) / Arr.every(array, predicate) |
array.find(...) | Arr.findFirst(array, predicate) returning Option<A> |
Object.values(record).reduce(...) | Record.reduce(record, zero, f) |
Object.values(record).filter/map(...) | Arr.filter / Arr.map over Record.values(record) |
Object.entries(record).filter/map/fromEntries(...) | Record.filter, Record.map, Record.collect, Record.fromEntries, or Record.toEntries |
[...items].sort(compare) | Arr.sort, Arr.sortWith, or Arr.sortBy with Order |
Configuration & Environment Variables
When to read: pull this in when reading configuration or environment variables via Effect'sConfigmodule, handling secrets withRedacted, providing custom config providers, or validating config values.
import { Config, ConfigProvider, Effect, Layer, Redacted } from "effect"
// Basic config values
const port = Config.number("PORT") // Required number
const host = Config.string("HOST").pipe( // Optional with default
Config.withDefault("localhost")
)
// Sensitive values (masked in logs)
const apiKey = Config.redacted("API_KEY") // Returns Redacted<string>
const secret = Redacted.value(yield* apiKey) // Unwrap when needed
// Nested configuration with prefix
const dbConfig = Config.all({
host: Config.string("HOST"),
port: Config.number("PORT"),
name: Config.string("NAME"),
}).pipe(Config.nested("DATABASE")) // DATABASE_HOST, DATABASE_PORT, etc.
// Using config in effects
const program = Effect.gen(function* () {
const p = yield* Config.number("PORT")
const key = yield* Config.redacted("API_KEY")
return { port: p, apiKey: Redacted.value(key) }
})
// Custom config provider (e.g., from object instead of env)
const customProvider = ConfigProvider.fromMap(
new Map([["PORT", "3000"], ["API_KEY", "secret"]])
)
const withCustomConfig = Effect.provide(
program,
Layer.setConfigProvider(customProvider)
)
// Config validation and transformation
const validPort = Config.number("PORT").pipe(
Config.validate({
message: "Port must be between 1 and 65535",
validation: (n) => n >= 1 && n <= 65535,
})
)Critical Rules for Effect-TS
These rules address common mistakes when working with Effect. Understanding why they matter helps write idiomatic Effect code.
INEFFECTIVE: try-catch in Effect.gen
Avoid `try-catch` blocks inside `Effect.gen` generators for handling Effect failures.
Effect failures are returned as exits, not thrown as JavaScript exceptions. Using try-catch will not catch Effect failures—it only catches synchronous throws from non-Effect code.
Problematic:
Effect.gen(function* () {
try {
const result = yield* someEffect;
} catch (error) {
// This catches synchronous throws only, NOT Effect failures
// Effect failures bypass this entirely
}
});Correct:
Effect.gen(function* () {
const result = yield* Effect.result(someEffect);
if (result._tag === "Failure") {
// Handle error case
}
});Alternative patterns:
Effect.catchAll/Effect.catchTagfor error recoveryEffect.resultto inspect success/failureEffect.tryPromise/Effect.tryfor wrapping external code
AVOID: Type Assertions
Avoid `as never`, `as any`, or `as unknown` type assertions.
These break TypeScript's type safety and hide real type errors. Always fix the underlying type issues instead.
Patterns to avoid:
const value = something as any;
const value = something as never;
const value = something as unknown;Correct approach:
- Use proper generic type parameters
- Import correct types from Effect
- Use proper Effect constructors and combinators
- Adjust function signatures to match usage
Note: This is general TypeScript guidance. Occasional assertions may be justified when interfacing with poorly-typed external libraries, but document the reason.
AVOID: Global Error in the Effect Error Channel
Do not model expected failures as Error in Effect.Effect<A, Error, R>. It erases domain information and weakens catchTag, Match, API error mapping, and serialization.
// Avoid
Effect.fail(new Error("User not found"))
// Prefer for domain/API errors
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
"UserNotFound",
{ userId: UserId }
) {}
Effect.fail(new UserNotFound({ userId }))Use Data.TaggedError for internal errors that do not need Schema decoding, encoding, annotations, or HTTP/OpenAPI integration.
AVOID: catchAllCause for Error Mapping
Cause includes both expected failures and defects. Mapping it into a normal error hides bugs that should stay defects.
// Avoid: catches defects too
effect.pipe(
Effect.catchAllCause((cause) =>
Effect.fail(new RepositoryError({ cause }))
)
)
// Prefer: transform only expected errors
effect.pipe(
Effect.mapError((error) =>
new RepositoryError({ cause: error })
)
)Reach for catchAllCause only when you intentionally need full cause inspection at a runtime/reporting boundary.
AVOID: Silent Error Swallowing
If a side effect matters, let its failure remain visible in the error channel. Audit logging, billing, persistence, security checks, and notification guarantees should not quietly become Effect.void.
// Avoid for important side effects
yield* audit.log(entry).pipe(
Effect.catchTag("AuditLogError", () => Effect.void)
)
// Prefer: propagate or map the error
yield* audit.log(entry).pipe(
Effect.mapError((error) => new CreateUserError({ cause: error }))
)Fallback values are fine for optional queries; swallowing side-effect failures is not.
AVOID: Effect Wrappers Around Safe Pure Code
Effect.try and Effect.tryPromise are boundary constructors. Do not wrap ordinary pure transformations just to make them "Effect-shaped".
// Avoid
const names = Effect.try(() => users.map((user) => user.name))
// Prefer
const names = users.map((user) => user.name)Use Effect.sync for synchronous effects with observable side effects, and Effect.try only for code that can throw.
RECOMMENDED: return yield* for Errors
*Use `return yield` when yielding errors or interrupts in Effect.gen for clarity.**
The runtime halts on failed yields regardless of return, but the explicit return makes termination obvious and prevents unreachable-code warnings.
Recommended:
Effect.gen(function* () {
if (someCondition) {
return yield* Effect.fail("error message");
}
if (shouldInterrupt) {
return yield* Effect.interrupt;
}
const result = yield* someOtherEffect;
return result;
});Acceptable but less clear:
Effect.gen(function* () {
if (someCondition) {
yield* Effect.fail("error message");
// Runtime halts here, but looks like code might continue
}
});The return keyword makes termination explicit and improves code readability.
Null vs Option<T> Rule
Use `Option<T>` internally, `T | null` at boundaries.
- Internal Effect computations →
Option<T> - React state/props →
T | null - JSON serialization →
T | nullorT | undefined - External API responses → normalize to
Option<T>at boundary
See option-null.md for comprehensive patterns.
Effect-Atom Reference
When to read: pull this in when using @effect-atom/*, React atom state backed by Effect, atom runtimes, atom families,stream-backed atoms, or Effectful mutation results in React.
Reactive state management library for Effect. Provides atoms (reactive state containers) that integrate with Effect's functional programming ecosystem and React.
Source code: https://github.com/tim-smart/effect-atom (open in browser)
Core API
Creating Atoms
import { Atom } from "@effect-atom/atom-react"
// Simple value atom
const countAtom = Atom.make(0)
// Derived atom (computed from other atoms)
const doubleAtom = Atom.make((get) => get(countAtom) * 2)
// Effectful atom (returns Result type)
const userAtom = Atom.make(
Effect.gen(function* () {
const api = yield* Api
return yield* api.fetchUser()
})
)
// Keep value when component unmounts (prevents reset)
const persistentAtom = Atom.make(0).pipe(Atom.keepAlive)React Hooks
import { useAtomValue, useAtomSet, useAtom } from "@effect-atom/atom-react"
function Counter() {
// Read-only access
const count = useAtomValue(countAtom)
// Write-only access
const setCount = useAtomSet(countAtom)
// Read and write access
const [value, setValue] = useAtom(countAtom)
return <button onClick={() => setCount((n) => n + 1)}>{count}</button>
}Atom Families
Generate stable atom references for dynamic keys:
const userAtomFamily = Atom.family((userId: string) =>
Atom.make(
Effect.gen(function* () {
const api = yield* Api
return yield* api.fetchUser(userId)
})
)
)
// Usage
const userAtom = userAtomFamily("user-123")Atom Functions
Create callable effects:
const incrementFn = Atom.fn(
Effect.gen(function* () {
const count = yield* Ref.get(counterRef)
yield* Ref.set(counterRef, count + 1)
})
)
// Invoke with useAtomSet
const increment = useAtomSet(incrementFn)
increment() // Returns Promise<Exit<...>>Atom Runtime
Create atom runtime from Effect layers for dependency injection:
const runtimeAtom = Atom.runtime(ApiLive)
function App() {
return (
<AtomProvider runtime={runtimeAtom}>
<MyComponent />
</AtomProvider>
)
}Advanced Features
URL Search Parameters
Bind atoms to URL search parameters:
const pageAtom = Atom.searchParam("page", {
decode: (s) => parseInt(s ?? "1", 10),
encode: (n) => n.toString(),
})Local Storage Persistence
const settingsAtom = Atom.kvs({
key: "app-settings",
defaultValue: { theme: "dark" },
})Scoped Resources
Add finalizers for cleanup when atom rebuilds or unmounts:
const websocketAtom = Atom.make((get) =>
Effect.gen(function* () {
const ws = yield* WebSocket.connect("wss://...")
yield* Effect.addFinalizer(() => ws.close())
return ws
})
)Event Listeners with Self-Update
const windowSizeAtom = Atom.make((get) =>
Effect.gen(function* () {
const handler = () =>
get.setSelf({ width: window.innerWidth, height: window.innerHeight })
window.addEventListener("resize", handler)
yield* Effect.addFinalizer(() =>
Effect.sync(() => window.removeEventListener("resize", handler))
)
return { width: window.innerWidth, height: window.innerHeight }
})
)Reactivity Keys
Trigger cache invalidation:
const dataAtom = Atom.make(
Effect.gen(function* () {
const keys = yield* Atom.withReactivity(["data-key"])
// Re-runs when "data-key" is invalidated
return yield* fetchData()
})
)RPC and HTTP API Integration
// RPC client
const rpcClient = AtomRpc.Tag()
// HTTP API client
const httpClient = AtomHttpApi.Tag()Result Handling
Effectful atoms return Result types. Handle with pattern matching:
function UserProfile() {
const userResult = useAtomValue(userAtom)
return Result.match(userResult, {
onSuccess: (user) => <div>{user.name}</div>,
onFailure: (error) => <div>Error: {error.message}</div>,
})
}Mutation Results
Use mode: "promiseExit" for mutation handling:
const saveUser = useAtomSet(saveUserAtom, { mode: "promiseExit" })
const handleSave = async () => {
const exit = await saveUser(userData)
if (Exit.isSuccess(exit)) {
// Handle success
}
}Streams
Pull values from streams:
const messagesAtom = Atom.pull(messageStream)Best Practices
1. Use `Atom.family` for dynamic keys — Generates stable references, avoids memory leaks 2. Apply `Atom.keepAlive` for persistent state — Prevents reset on unmount 3. Use `Atom.runtime` for dependency injection — Integrates Effect layers with React context 4. Implement finalizers for cleanup — Ensures proper resource management 5. Use `mode: "promiseExit"` for mutations — Provides typed success/failure handling 6. Prefer derived atoms over component state — Keeps state logic centralized
Effect + Next.js Integration
When to read: pull this in when using @prb/effect-next, Next.js App Router handlers/actions/middleware, request-scopedruntime caches, Effect React hooks, or the @prb/effect-next testing kit.@prb/effect-next provides typed helpers for integrating Effect with Next.js 15+ App Router—route handlers, server actions, middleware, and React hooks.
Core API
Route Handlers
// app/api/users/[id]/route.ts
import { effectHandler } from "@prb/effect-next/handlers";
import { Effect } from "effect";
import { RouteParams } from "@prb/effect-next/params";
export const GET = effectHandler(
Effect.gen(function* () {
const params = yield* RouteParams;
const user = yield* fetchUser(params.id);
return Response.json(user);
}),
AppLayer
);Server Actions
"use server";
import { effectAction } from "@prb/effect-next/action";
import { Effect } from "effect";
export const createUser = effectAction(
Effect.gen(function* () {
const db = yield* Database;
return yield* db.insert(users).values({ name: "Alice" });
}),
AppLayer
);
// Returns Exit-like result with _tag: "Success" | "Failure"
const result = await createUser();
if (result._tag === "Success") {
console.log(result.value);
}Middleware
// middleware.ts
import { effectMiddleware } from "@prb/effect-next/middleware";
import { Effect, Layer } from "effect";
import { Headers } from "@prb/effect-next/headers";
const AuthLayer = Layer.effect(
AuthService,
Effect.gen(function* () {
const headers = yield* Headers;
const token = headers.get("authorization");
if (!token) yield* Effect.fail({ _tag: "Unauthorized" });
return { validateToken: () => Effect.succeed(true) };
})
);
export const middleware = effectMiddleware(
Effect.gen(function* () {
yield* AuthService;
return NextResponse.next();
}),
AuthLayer
);React Hooks
Client-side hooks for running Effects in React components.
"use client";
import {
EffectNextProvider,
useEffectNextRuntime,
useEffectMemo,
useEffectOnce,
useForkEffect,
useStream,
useStreamLatest,
useSubscriptionRef
} from "@prb/effect-next/react-hooks";
// Wrap app with provider
<EffectNextProvider runtime={runtime}>{children}</EffectNextProvider>;
// Access runtime
const runtime = useEffectNextRuntime();
// Run Effect with dependencies (like useMemo)
const data = useEffectMemo(() => effect, [deps], runtime);
// Run Effect once on mount
const data = useEffectOnce(effect, runtime);
// Run Effect in background (fire-and-forget)
useForkEffect(effect, runtime, [deps]);
// Subscribe to Stream
const values = useStream(stream, runtime);
const latest = useStreamLatest(stream, runtime, initialValue);
// Subscribe to SubscriptionRef
const value = useSubscriptionRef(ref, runtime);Request-Scoped Cache
Leverage React's cache() for request deduplication.
import { reactCache, reactCacheFn, reactCacheWithKey } from "@prb/effect-next/cache";
import { ManagedRuntime } from "effect";
const runtime = ManagedRuntime.make(AppLayer);
// Cache an Effect
export const getUsers = reactCache(
Effect.gen(function* () {
const db = yield* Database;
return yield* db.query("SELECT * FROM users");
}),
runtime
);
// Cache a function with arguments
export const getUserById = reactCacheFn((id: string) =>
Effect.gen(function* () {
const db = yield* Database;
return yield* db.query(`SELECT * FROM users WHERE id = ${id}`);
}),
runtime
);
// Cache with custom key
export const getUser = reactCacheWithKey(
(opts) => fetchUserEffect(opts),
(opts) => `user:${opts.id}`,
runtime
);Headers & Cookies
import { Headers, Cookies } from "@prb/effect-next/headers";
Effect.gen(function* () {
const headers = yield* Headers;
const userAgent = headers.get("user-agent");
const cookies = yield* Cookies;
const sessionId = cookies.get("sessionId");
});Params
import { RouteParams, SearchParams } from "@prb/effect-next/params";
Effect.gen(function* () {
const params = yield* RouteParams;
const userId = params.id;
const searchParams = yield* SearchParams;
const page = searchParams.page;
});Navigation
import { redirect, rewrite, notFound } from "@prb/effect-next/navigation";
Effect.gen(function* () {
yield* redirect("/login");
yield* rewrite("/new-path");
yield* notFound();
});Testing Kit
import {
assertRight,
assertLeft,
expectTaggedFailure,
expectDefect,
runExpectSuccess,
runExpectFailure,
makeMockRuntime
} from "@prb/effect-next/testing-kit";
// Assert success
test("should succeed", async () => {
const exit = await Effect.runPromiseExit(effect);
const value = assertRight(exit);
expect(value).toBe(42);
});
// Assert specific failure tag
test("should fail with NotFound", async () => {
const exit = await Effect.runPromiseExit(effect);
expectTaggedFailure(exit, "NotFound");
});
// Run and expect success
test("should create user", async () => {
const user = await runExpectSuccess(createUser(), runtime);
expect(user.name).toBe("Alice");
});Best Practices
1. Use `Effect.fn()` — Automatic telemetry spans and better stack traces 2. Centralize layers — Create AppLayer with all shared services 3. Error handling — Use .catchAll() or .catchTag() for Effect-level errors 4. Request caching — Use reactCache for request-scoped memoization 5. Server-only Effect — Effect-ts shines server-side; avoid complex Effect in client components
Option vs Null Patterns
When to read: pull this in when deciding whether absence should be represented asOption<A>,A | null, or
A | undefined, especially at JSON/API/React/storage boundaries.The Rule
Use Option<T> for Effect domain logic. Use T | null only at external boundaries.
When to Use Option<T>
- Internal Effect computations
- Domain models where absence has meaning
- Function returns that may not produce a value
- Chain operations that may fail to produce a value
When to Use T | null
- React state/props (hooks expect nullable primitives)
- JSON serialization (Option doesn't serialize to JSON)
- External API responses
- Database query results
- localStorage/sessionStorage values
Boundary Normalization
import { Option } from "effect"
// Incoming: null → Option (at API/storage boundary)
const fromApi = Option.fromNullable(response.data)
const fromStorage = Option.fromNullable(localStorage.getItem("key"))
// Outgoing: Option → null (for React/JSON)
const toReact = Option.getOrNull(maybeValue)
const toJson = Option.getOrUndefined(maybeValue)Common Patterns
// Map over optional value
Option.map(maybeUser, (user) => user.name)
// Chain optional operations
Option.flatMap(maybeUser, (user) => Option.fromNullable(user.profile))
// Provide default
Option.getOrElse(maybeValue, () => defaultValue)
// Check and extract
if (Option.isSome(maybeValue)) {
console.log(maybeValue.value) // Safe access
}Avoid Option\<Option<T>> Creep
// WRONG: Nested options from repeated normalization
const bad = Option.fromNullable(Option.fromNullable(x))
// RIGHT: Normalize once at the boundary
const good = Option.fromNullable(x)
// If you have nested options, flatten them
const flattened = Option.flatten(nestedOption)Schema Decoding
import { Schema } from "effect"
// Optional field with Option type
const UserSchema = Schema.Struct({
name: Schema.String,
nickname: Schema.optionalWith(Schema.String, { as: "Option" })
})
// nickname will be Option<string>
// Optional field with null (for JSON compat)
const ApiUserSchema = Schema.Struct({
name: Schema.String,
nickname: Schema.NullOr(Schema.String)
})
// nickname will be string | nullEffect-Atom Integration
// Atoms with nullable state (for React compat)
const userAtom = Atom.make<User | null>(null)
// Convert at boundaries
const program = Effect.gen(function* () {
const maybeUser = yield* fetchUser() // Returns Option<User>
return Option.getOrNull(maybeUser) // Convert for React
})Pattern Matching (Match Module)
When to read: pull this in when you need Effect's Match module to handle tagged unions, replace nested catchTag chains, or pattern-match on values exhaustively.
Default branching tool for tagged unions and complex conditionals.
import { Match } from "effect"
// Type-safe exhaustive matching on tagged errors
const handleError = Match.type<AppError>().pipe(
Match.tag("UserCancelledError", () => null), // Expected rejection
Match.tag("ValidationError", (e) => e.message), // Domain error
Match.tag("NetworkError", () => "Connection failed"), // Retryable
Match.exhaustive // Compile error if case missing
)
// Replace nested catchTag chains
// BEFORE: effect.pipe(catchTag("A", ...), catchTag("B", ...), catchTag("C", ...))
// AFTER:
Effect.catchAll(effect, (error) =>
Match.value(error).pipe(
Match.tag("A", handleA),
Match.tag("B", handleB),
Match.tag("C", handleC),
Match.exhaustive
)
)
// Match on values (cleaner than if/else)
const describe = Match.value(status).pipe(
Match.when("pending", () => "Loading..."),
Match.when("success", () => "Done!"),
Match.orElse(() => "Unknown")
)Data.taggedEnum Matching
Use the constructor $match helper for Data.taggedEnum unions when you want exhaustiveness and variant-specific payload types without casts.
As of effect@3.21.3, $match(value, cases) preserves generic type parameters inside each arm for generic tagged enums.
import { Data } from "effect"
type Tree<A> = Data.TaggedEnum<{
Leaf: { readonly value: A }
Branch: { readonly children: ReadonlyArray<Tree<A>> }
}>
interface TreeDefinition extends Data.TaggedEnum.WithGenerics<1> {
readonly taggedEnum: Tree<this["A"]>
}
const Tree = Data.taggedEnum<TreeDefinition>()
const collect = <A>(tree: Tree<A>): ReadonlyArray<A> =>
Tree.$match(tree, {
Leaf: (leaf) => [leaf.value],
Branch: (branch) => branch.children.flatMap(collect<A>)
})Prefer this over switch plus as casts when recursive generic variants are involved.
Platform and RPC
When to read: pull this in when using@effect/platform,HttpLayerRouter,@effect/rpc, msgpack serialization, or
deployment runtimes such as Cloudflare Workers.
HttpLayerRouter Middleware
@effect/platform@0.96.1 preserves fiber context in HttpLayerRouter.addHttpApi, so API-level middleware is applied when an API is registered through the router.
If API-level middleware appears to be skipped, check the pinned @effect/platform version before adding route-local workarounds.
RPC MsgPack Serialization
@effect/rpc@0.75.1 adds configurable msgpack serialization:
import { RpcSerialization } from "@effect/rpc"
const serialization = RpcSerialization.makeMsgPack({
useRecords: true
})The default RpcSerialization.msgPack is equivalent to makeMsgPack({ useRecords: true }).
Cloudflare Workers
The current RPC stack uses msgpackr@1.11.10, which falls back when dynamic code evaluation is blocked. In Cloudflare Workers or similarly restricted runtimes, prefer upgrading @effect/rpc / @effect/platform over custom serialization patches for silent msgpack decode failures.
Quick Utilities (Functions, Deprecations)
When to read: pull this in when using small utility helpers like constVoid, or checking the running list of Effectdeprecations.
Utility Functions
import { constVoid as noop } from "effect/Function"
// constVoid returns undefined, useful as a no-operation callback
noop() // undefined
// Common use cases:
Effect.tap(effect, noop) // Ignore value, just run effect
Promise.catch(noop) // Swallow errors
eventEmitter.on("event", noop) // Register empty handlerDeprecations
- `BigDecimal.fromNumber` — Use
BigDecimal.unsafeFromNumberinstead (3.11.0+) - `Schema.annotations()` — Now removes previously set identifier annotations; identifiers are tied to the schema's
ast reference only (3.17.10)
Recent Upstream Notes
When to read: pull this in when checking whether local guidance is stale against ~/.effect or when a behavior changedrecently.
Checked against ~/.effect HEAD 05d72eab7 from 2026-06-05.
| Package | Version | Public guidance reflected here |
|---|---|---|
effect | 3.21.3 | Generic Data.taggedEnum $match; closed Schema.Never records |
@effect/ai | 0.36.0 | Tool.EmptyParams and omitted tool parameters |
@effect/ai-openai | 0.40.0 | OpenAI strict handling, prompt cache enum, output dedup behavior |
@effect/platform | 0.96.1 | HttpLayerRouter.addHttpApi applies API-level middleware |
@effect/sql | 0.51.1 | SqlSchema row decoding helpers and repository boundary guidance |
@effect/rpc | 0.75.1 | RpcSerialization.makeMsgPack(options) and Cloudflare msgpack fix |
@effect/cluster | 0.59.0 | Shard group routing and SQL advisory lock numbering fixes |
@effect/workflow | 0.18.2 | Child workflow parent pointer forwarded with discard: true |
@effect/cli | latest | Completion command hyphen replacement and dark-terminal help spans |
Before updating this file, prefer package changelogs under ~/.effect/packages/*/CHANGELOG.md over inferring from commit titles alone.
Runtime Patterns
When to read: pull this in when working with resource lifecycles, durations, scheduling/retry/repeat, mutable state (Ref / Deferred), reactive references (SubscriptionRef), or concurrency primitives (fork, race, Fiber).
Resource Management
Effect.acquireUseRelease(acquire, use, release) // Bracket pattern
Effect.scoped(effect) // Scope lifetime to effect
Effect.addFinalizer(cleanup) // Register cleanup actionDuration
Effect accepts human-readable duration strings anywhere a DurationInput is expected:
// String syntax (preferred) - singular or plural forms work
Duration.toMillis("5 minutes") // 300000
Duration.toMillis("1 minute") // 60000
Duration.toMillis("30 seconds") // 30000
Duration.toMillis("100 millis") // 100
// Verbose syntax (avoid)
Duration.toMillis(Duration.minutes(5)) // Same result, more verbose
// Common units: millis, seconds, minutes, hours, days, weeks
// Also: nanos, microsScheduling
Effect.retry(effect, Schedule.exponential("100 millis")) // Retry with backoff
Effect.repeat(effect, Schedule.fixed("1 second")) // Repeat on schedule
Schedule.compose(s1, s2) // Combine schedulesState Management
Ref.make(initialValue) // Mutable reference
Ref.get(ref) // Read value
Ref.set(ref, value) // Write value
Deferred.make<E, A>() // One-time async valueSubscriptionRef (Reactive References)
// WARNING: Never use unsafeMake - it may not exist in your Effect version.
// If you see "unsafeMake is not a function", use the safe API below.
SubscriptionRef.make(initial) // Create reactive reference (safe)
SubscriptionRef.get(ref) // Read current value
SubscriptionRef.set(ref, value) // Update value (notifies subscribers)
SubscriptionRef.changes(ref) // Stream of value changes
// React integration (effect-atom pattern)
const ref = yield* SubscriptionRef.make<User | null>(null)
// Hook reads: useSubscriptionRef(ref) — returns current value or null
// Handle null explicitly in componentsConcurrency
Effect.fork(effect) // Run in background fiber
Fiber.join(fiber) // Wait for fiber result
Effect.race(effect1, effect2) // First to complete wins
Effect.all([...effects], { concurrency: "unbounded" })Schema and JSON Schema
When to read: pull this in when decoding external data, generating JSON Schema, building AI tool parameter schemas,
or debugging closed object shapes.
Decode at the Boundary
Keep Schema at IO boundaries. Decode unknown input once, then pass typed domain values through the rest of the program.
import { Effect, Schema } from "effect"
const User = Schema.Struct({
id: Schema.Number,
name: Schema.String
})
const decodeUser = Schema.decodeUnknown(User)
const program = (input: unknown) =>
Effect.gen(function* () {
const user = yield* decodeUser(input)
return user.name
})Domain and API Models
Prefer Schema.Class for domain entities, API request/response bodies, and values that need construction, validation, encoding, annotations, or structural equality.
import { Schema } from "effect"
const UserId = Schema.NonEmptyTrimmedString.pipe(Schema.brand("UserId"))
type UserId = typeof UserId.Type
class User extends Schema.Class<User>("User")({
id: UserId,
email: Schema.NonEmptyTrimmedString,
displayName: Schema.NonEmptyTrimmedString
}) {
get label() {
return `${this.displayName} <${this.email}>`
}
}
const user = User.make({
id: UserId.make("user_123"),
email: "ada@example.com",
displayName: "Ada"
})Use precise domain schemas instead of weakening fields to primitives. If a field can be UserId, CurrencyCode, or a domain literal union, do not type it as Schema.String unless the boundary genuinely accepts any string.
Schema-Backed Domain Errors
Use Schema.TaggedError when errors cross module, API, persistence, or serialization boundaries. This gives you tagged matching plus Schema-derived validation, annotations, and type guards.
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
"UserNotFound",
{ userId: UserId }
) {
get message() {
return `User not found: ${this.userId}`
}
}
const isUserNotFound = Schema.is(UserNotFound)Data.TaggedError remains useful for internal-only errors that do not need decoding, encoding, OpenAPI annotations, or Schema type guards.
Optional, Nullable, and Exact Fields
- Use
Schema.optionalWith(schema, { as: "Option" })for optional domain fields that should decode toOption<A>. - Use
Schema.NullOr(schema)when the serialized form explicitly usesnull. - Use exact optional fields when extra keys must be rejected in generated JSON Schema.
- Avoid
*FromSelfvariants for JSON/API shapes unless you intentionally want already-decoded input. Standard variants
such as Schema.Option(...) encode and decode cleanly at boundaries.
const ApiUser = Schema.Struct({
id: Schema.Number,
nickname: Schema.NullOr(Schema.String),
displayName: Schema.optionalWith(Schema.String, { exact: true })
})Closed Empty Records
As of effect@3.21.3, JSON Schema generation emits additionalProperties: false for string-keyed records whose values are Schema.Never.
Use this shape when you need an object schema that accepts no dynamic properties, especially for no-parameter AI tools:
import { Schema } from "effect"
const NoParams = Schema.Record({
key: Schema.String,
value: Schema.Never
})That record represents an empty object parameter shape:
{}is valid.{ anything: null }is invalid.
Prefer a named NoParams schema or Tool.EmptyParams over ad hoc {} types when a JSON Schema consumer needs the closed shape.
JSON Schema Generation
For direct JSON Schema generation, inspect ~/.effect/packages/effect/src/JSONSchema.ts and use the public JSONSchema.fromAST API with the schema AST.
import { JSONSchema, Schema } from "effect"
const jsonSchema = JSONSchema.fromAST(NoParams.ast, {
definitions: {},
topLevelReferenceStrategy: "skip"
})If generation fails, look for unsupported schema nodes or missing JSON Schema annotations before weakening the domain schema.
Decode Effects, Not Sync Throws
Prefer Schema.decodeUnknown(schema) in Effect code. Avoid sync decoders at effectful boundaries because they throw exceptions instead of returning typed failures.
const decodeUser = Schema.decodeUnknown(User)
const handle = (input: unknown) =>
Effect.gen(function* () {
const user = yield* decodeUser(input)
return user.id
})Services and Layers
When to read: pull this in when defining or composing Effect services, choosing betweenContext.Tag/Effect.Service/Context.Reference/Context.ReadonlyTag, or writing generator-based effects withEffect.genandEffect.fn.
Services and Layers
// Pattern 1: Context.Tag (implementation provided separately via Layer)
class MyService extends Context.Tag("MyService")<MyService, { ... }>() {}
const MyServiceLive = Layer.succeed(MyService, { ... })
Effect.provide(effect, MyServiceLive)
// Pattern 2: Effect.Service (default implementation bundled)
class UserRepo extends Effect.Service<UserRepo>()("UserRepo", {
effect: Effect.gen(function* () {
const db = yield* Database
return { findAll: db.query("SELECT * FROM users") }
}),
dependencies: [Database.Default], // Optional service dependencies
accessors: true // Auto-generate method accessors
}) {}
Effect.provide(effect, UserRepo.Default) // .Default layer auto-generated
// Use UserRepo.DefaultWithoutDependencies when deps provided separately
// Effect.Service with parameters (3.16.0+)
class ConfiguredApi extends Effect.Service<ConfiguredApi>()("ConfiguredApi", {
effect: (config: { baseUrl: string }) =>
Effect.succeed({ fetch: (path: string) => `${config.baseUrl}/${path}` })
}) {}
// Pattern 3: Context.Reference (defaultable tags - 3.11.0+)
class SpecialNumber extends Context.Reference<SpecialNumber>()(
"SpecialNumber",
{ defaultValue: () => 2048 }
) {}
// No Layer required if default value suffices
// Pattern 4: Context.ReadonlyTag (covariant - 3.18.0+)
// Use for functions that consume services without modifying the type
function effectHandler<I, A, E, R>(service: Context.ReadonlyTag<I, Effect.Effect<A, E, R>>) {
// Handler can use service in a covariant position
}Global Context vs Per-Request Context
Use Layers for long-lived dependencies wired at startup: config, clients, repositories, and services. Use Effect.provideService for per-request values such as authenticated user, tenant, organization, locale, request id, or authorization context.
const handleRequest = (request: Request) =>
program.pipe(
Effect.provideService(CurrentUserId, extractUserId(request)),
Effect.provideService(RequestId, extractRequestId(request))
)Avoid constructing a Layer for one request's data. Per-request values are not application services, and wrapping them in Layer.succeed makes the runtime boundary harder to see.
Layer Construction
Choose the layer constructor by lifecycle:
Layer.succeed(Tag, value) // Static pure value, common for tests and simple constants
Layer.effect(Tag, make) // Effectful construction without cleanup
Layer.scoped(Tag, acquire) // Resourceful construction with cleanup
Layer.unwrapEffect(makeLayer) // Effectfully builds a LayerFor live services that read dependencies, config, or allocate resources, prefer Layer.effect or Layer.scoped over prebuilding a value and hiding acquisition in Layer.succeed.
Layer Memoization
Layers are memoized by object identity. Reusing the same layer object in one composition shares one instance; creating a new layer object creates a distinct instance.
const Shared = Layer.effect(Client, makeClient)
const oneClient = Layer.mergeAll(Shared, Shared)
const twoClients = Layer.mergeAll(
Layer.effect(Client, makeClient),
Layer.effect(Client, makeClient)
)Use Layer.fresh(layer) only when you need to escape memoization for the same layer reference, such as a module-level constant live layer reused with different test configuration. Do not wrap factory-created test layers in Layer.fresh; each factory call already returns a new layer object.
Generator Pattern
Effect.gen(function* () {
const a = yield* effectA;
const b = yield* effectB;
if (error) {
return yield* Effect.fail(new MyError());
}
return result;
});
// Effect.fn - automatic tracing and telemetry (preferred for named functions)
const fetchUser = Effect.fn("fetchUser")(function* (id: string) {
const db = yield* Database
return yield* db.query(id)
})
// Creates spans, captures call sites, provides better stack tracesEffect SQL
When to read: pull this in when using@effect/sql,SqlSchema, repository services, database transactions, or row
decoding.
Decode Rows with Schema
Prefer SqlSchema or explicit Schema decoding over TypeScript type parameters on raw SQL calls. A type parameter can describe the row shape, but it does not validate database output.
import { SqlClient, SqlSchema } from "@effect/sql"
import { Context, Effect, Option, Schema } from "effect"
const AccountRow = Schema.Struct({
id: AccountId,
name: Schema.String,
accountType: AccountType
})
const findById = SqlSchema.findOne({
Request: AccountId,
Result: AccountRow,
execute: (id) =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
return yield* sql`
SELECT id, name, account_type AS "accountType"
FROM accounts
WHERE id = ${id}
`
})
})Use precise domain schemas for rows. If the database value should be an AccountId, CurrencyCode, BigDecimal, or domain literal union, decode it as that type instead of weakening it to Schema.String or Schema.Number.
Pick the Right SqlSchema Helper
SqlSchema.findOne({ Request, Result, execute }) // Option<A>; zero or one row
SqlSchema.findAll({ Request, Result, execute }) // Array<A>; zero or more rows
SqlSchema.single({ Request, Result, execute }) // A; exactly one row
SqlSchema.void({ Request, execute }) // void; writes with no returned rowUse findOne when absence is a normal case, then convert Option.none to a domain error at the service boundary if the caller requires existence.
Repository Boundaries
Keep SQL details in repository services. Domain services should depend on repository tags and speak in domain values, not raw rows.
class AccountRepository extends Context.Tag("AccountRepository")<
AccountRepository,
{
readonly findById: (id: AccountId) => Effect.Effect<Option.Option<Account>, RepositoryError>
readonly save: (account: Account) => Effect.Effect<void, RepositoryError>
}
>() {}Map driver and decode errors into repository errors with Effect.mapError; do not use catchAllCause unless you are at a deliberate reporting boundary and want to include defects.
Transactions
Use the SQL client's transaction API around all writes that must commit atomically. Keep audit, outbox, or ledger writes in the same transaction when the product invariant requires them to succeed or fail together.
const createAccount = (account: Account) =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
yield* sql.withTransaction(
Effect.gen(function* () {
yield* insertAccount(account)
yield* insertAuditEntry(account)
})
)
})Stream Patterns
When to read: pull this in when creating, transforming, consuming, testing, or resource-managing Effect Streams,especially when the stream may be infinite or backpressured.
Streams are lazy, pull-based sequences of values that can be infinite. Handle with care.
Create Streams
import { Stream } from "effect"
// From values
Stream.make(1, 2, 3)
// From iterable
Stream.fromIterable([1, 2, 3])
// Single value from effect
Stream.fromEffect(fetchUser())
// Infinite stream from repeated effect
Stream.repeatEffect(Effect.sync(() => Math.random()))
// From async iterable
Stream.fromAsyncIterable(asyncGenerator(), (error) => new StreamError({ cause: error }))
// Chunks for efficiency
Stream.fromChunk(Chunk.make(1, 2, 3))Consume Streams
// Collect all values (DANGEROUS for infinite streams)
const allValues = yield* Stream.runCollect(stream) // Returns Chunk<A>
// Process each element
yield* Stream.runForEach(stream, (value) => Effect.log(`Got: ${value}`))
// Fold/reduce
const sum = yield* Stream.runFold(stream, 0, (acc, n) => acc + n)
// First element only
const first = yield* Stream.runHead(stream) // Returns Option<A>
// Drain (run for side effects, discard values)
yield* Stream.runDrain(stream)Bound Consumption (Critical for Safety)
// WRONG: Hangs forever on infinite stream
yield* Stream.runCollect(infiniteStream)
// RIGHT: Take first N elements
yield* Stream.runCollect(Stream.take(infiniteStream, 100))
// RIGHT: Take until condition
yield* Stream.runCollect(Stream.takeUntil(stream, (x) => x > 100))
// RIGHT: Take while condition holds
yield* Stream.runCollect(Stream.takeWhile(stream, (x) => x < 100))
// RIGHT: Apply timeout
yield* Stream.runCollect(stream).pipe(Effect.timeout("5 seconds"))Transform Streams
// Map values
Stream.map(stream, (x) => x * 2)
// Filter values
Stream.filter(stream, (x) => x > 0)
// FlatMap (each value produces a stream)
Stream.flatMap(userIds, (id) => Stream.fromEffect(fetchUser(id)))
// Tap for side effects
Stream.tap(stream, (x) => Effect.log(`Processing: ${x}`))
// Scan (running fold)
Stream.scan(stream, 0, (acc, x) => acc + x) // Emits running totalsChunk and Batch
// Group into chunks of N
Stream.grouped(stream, 100) // Stream<Chunk<A>>
// Group by time window
Stream.groupedWithin(stream, 100, "1 second")
// Rechunk for efficiency
Stream.rechunk(stream, 1000)Handle Errors in Streams
// Catch errors and recover
Stream.catchAll(stream, (error) => Stream.make(fallbackValue))
// Retry on failure
Stream.retry(stream, Schedule.exponential("100 millis"))
// Handle specific error tags
Stream.catchTag(stream, "NetworkError", (e) => Stream.empty)Resource Safety
// Bracket pattern for streams
Stream.acquireRelease(
acquire, // Effect<Resource, E, R>
release // (resource: Resource) => Effect<void>
)
// Scoped stream (resource released when stream completes)
Stream.scoped(Effect.acquireRelease(open, close))
// Finalizer
Stream.ensuring(stream, cleanup)Common Gotchas
1. Infinite streams: Always bound consumption with take, takeUntil, or timeout 2. Backpressure: Streams are pull-based; slow consumers automatically apply backpressure 3. Resource leaks: Use scoped/bracket patterns for resources 4. Chunking overhead: Rechunk for better performance with small items 5. Error propagation: Errors terminate the stream; use catchAll to recover
Testing Effect-TS (Vitest) — Reference
When to read: pull this in when using@effect/vitest,it.effect,TestClock,Effect.sleep, retry schedules,
streams, background fibers, scoped resources, or Effect runtime boundaries in tests.
This is a pragmatic guide for writing _deterministic_ tests in Effect-TS codebases, especially when using @effect/vitest.
The #1 gotcha: it.effect uses TestClock
@effect/vitest's it.effect runs your test with a TestContext (including `TestClock`).
Implications:
- Time starts at 0.
- Time does not pass unless you advance it.
- Any
Effect.sleep(...),Schedule.spaced(...), retry backoff, polling loop, etc. will stall forever unless you callTestClock.adjust(...).
Use it.live when you truly want wall-clock time.
Time: don't use Date.now() in Effect code
If production code uses Date.now(), it becomes hard (or impossible) to test deterministically under TestClock.
Prefer Effect's clock service:
import { Clock, Effect } from "effect";
const nowMillis = Clock.currentTimeMillis;
const program = Effect.gen(function* () {
const now = yield* Clock.currentTimeMillis;
return now;
});That makes your code controllable via TestClock.
Fractional Milliseconds and Unsafe Nanos
effect@3.21.2 fixed TestClock.unsafeCurrentTimeNanos() to floor fractional millisecond instants before converting to BigInt.
yield* TestClock.setTime(199023438.0000004)
const testClock = yield* TestClock.testClock()
testClock.unsafeCurrentTimeNanos() // 199023438000000nStill prefer Clock.currentTimeMillis, Clock.currentTimeNanos, or DateTime.now inside Effect code. Reach for unsafe clock reads only in low-level tests where the synchronous escape hatch is the thing under test.
Replace Effect.sleep with TestClock.adjust (under it.effect)
Instead of:
yield * Effect.sleep("50 millis");do:
import { TestClock } from "effect";
yield * TestClock.adjust("50 millis");If you _must_ use real timers (e.g. testing integration with Node timers), switch the whole test to it.live.
Testing retries / backoff / scheduled loops
Retry schedules and Schedule.spaced(...) don't progress under TestClock unless you advance time.
A reliable pattern is:
import { Effect, Fiber, TestClock } from "effect";
const runWithTime = <A, E, R>(
effect: Effect.Effect<A, E, R>,
adjust: Parameters<typeof TestClock.adjust>[0] = "1000 millis"
) =>
Effect.gen(function* () {
const fiber = yield* Effect.fork(effect);
yield* TestClock.adjust(adjust);
return yield* Fiber.join(fiber);
});Advance _enough_ time for the whole schedule/backoff chain to complete.
Streams, watches, and background fibers: always bound + cleanup
Most test “hangs” in Effect come from one of these:
- A stream that never ends (
Stream.runCollect(stream)on an infinite stream) - A watch/polling loop forked and never interrupted
- A scoped resource that never gets finalized because the scope never closes
Recommendations:
- Prefer bounded consumption:
Stream.take(stream, n)/Stream.takeUntil(...). - If you fork a fiber, ensure it is interrupted on all paths:
yield* Fiber.interrupt(fiber)- or run it inside a
Scopeand let scope finalizers do the cleanup. - Consider
Effect.timeout(...)/Effect.timeoutFail(...)around anything that could block.
Concurrency gotcha: Effect.fork does not mean “the fiber has started”
When you write a test like:
- fork 2–3 fibers
- then immediately
Deferred.succeed(gate, ...)
…you have not guaranteed that the forked fibers have reached the code you intend to coordinate (e.g. Deferred.await(gate)).
Effect.fork creates a fiber and schedules it, but the scheduler may not run it until later. If you open the gate too early:
- each fiber can observe the gate as already-open
- your “concurrent” test can become effectively sequential
- assertions like “underlying effect executed once” can fail intermittently even though the implementation is correct
Deterministic pattern: started latch + gate
If you need to ensure real overlap, add a second Deferred that the underlying effect completes as soon as it begins:
import { Deferred, Effect, Fiber } from "effect";
Effect.gen(function* () {
let executions = 0;
const started = yield* Deferred.make<void>();
const gate = yield* Deferred.make<void>();
const underlying = Effect.gen(function* () {
executions++;
// Signal we actually started executing (at least one fiber is “in” now)
yield* Deferred.succeed(started, undefined);
// Block here to force overlap
yield* Deferred.await(gate);
return "ok";
});
const f1 = yield* Effect.fork(underlying);
const f2 = yield* Effect.fork(underlying);
// Don't open the gate until at least one fiber definitely started
yield* Deferred.await(started);
yield* Deferred.succeed(gate, undefined);
yield* Fiber.join(f1);
yield* Fiber.join(f2);
// Now it's safe to assert expectations about overlap / dedup / sharing
// expect(executions).toBe(1)
});This avoids “we opened the gate before any fiber ran” flakiness and makes concurrency assertions reliable.
Use it.scoped when your test allocates scoped resources
If your test (or the code under test) uses Effect.acquireRelease, Stream.asyncScoped, resourceful Layers, etc., prefer it.scoped / it.scopedLive so finalizers are guaranteed to run when the test completes.
Don't “escape” the test runtime inside an Effect test
Avoid calling Effect.runPromise(...) (or similar “run” APIs) _inside_ an it.effect program to drive internal logic. It can accidentally run work on a different runtime (e.g. a live clock), defeating TestClock determinism.
Prefer staying inside the Effect you're already running:
- pass
Effects around andyield*them - if you truly need a Promise boundary, do it at the test boundary, not mid-program
Quick decision table
- Uses timeouts/sleeps/retries/polling? →
it.effect+TestClock.adjust(...) - Needs wall clock / Node timers / real delays? →
it.live(orit.scopedLive) - Allocates resources that must be finalized? →
it.scoped/it.scopedLive
Related skills
How it compares
Use effect-ts for Effect-native Config and Redacted patterns; use dotenv snippets for non-Effect Node projects.
FAQ
When must I clone ~/.effect?
When upstream API details, changelogs, or complex type errors need package source inspection.
What is the first file to read?
references/critical-rules.md before writing or changing Effect code.
Does every import need this skill?
No. Use it when the change depends on Effect semantics, not trivial touch points.
Is Effect Ts safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.