Use this file to discover all available pages before exploring further.
Anti-pattern diagnostics highlight code that technically compiles but is considered problematic: it may cause subtle bugs, break service lifetimes, or obscure intent. Most are warnings or suggestions.
Rules marked ⚠️ are warnings by default. Rules marked 💡 are suggestions. Rules marked ➖ are off by default.
catchUnfailableEffect — 💡 suggestion
V3/V4: ✓ / ✓Warns when error-handling operators like Effect.catchAll, Effect.catch, or Effect.mapError are applied to an Effect whose error channel is never. The handler can never be triggered, which signals a logic error or dead code.
import { Effect } from "effect"// ❌ Violation — Effect.succeed never fails; catchAll is unreachableconst result = Effect.succeed(42).pipe( Effect.catchAll((e) => Effect.succeed(0)) // never triggered)// ✅ Correct — only add error handling when the effect can failconst result = Effect.succeed(42)
effectFnIife — ⚠️ warning + 🔧 quick fix
V3/V4: ✓ / ✓Effect.fn and Effect.fnUntraced create reusable named functions. Calling them immediately as an IIFE (immediately invoked function expression) defeats that purpose — use Effect.gen directly instead.
import { Effect } from "effect"// ❌ Violation — Effect.fn called immediately, not reusedconst result = Effect.fn("doWork")(function* () { return yield* Effect.succeed(42)})()// ✅ Correct — use Effect.gen when you don't need a reusable functionconst result = Effect.gen(function* () { return yield* Effect.succeed(42)})
The quick fix replaces the Effect.fn(...)() call with Effect.gen.
effectGenUsesAdapter — ⚠️ warning
V3/V4: ✓ / ✓Warns when the deprecated first-parameter adapter (_) is used inside Effect.gen. The adapter was required in older versions of Effect but is now just an alias for pipe and should be removed.
import { Effect } from "effect"// ❌ Violation — adapter parameter is no longer neededconst program = Effect.gen(function* (_) { const value = yield* _(Effect.succeed(42)) return value})// ✅ Correct — drop the adapter and use yield* directlyconst program = Effect.gen(function* () { const value = yield* Effect.succeed(42) return value})
effectInFailure — ⚠️ warning
V3/V4: ✓ / ✓Warns when an Effect value appears in the error channel (E) of another Effect. The failure channel is intended to hold failure types (plain data), not executable computations. An Effect in the failure channel is never run and suggests a design mistake.
import { Effect } from "effect"// ❌ Violation — Effect in the error channel is never executedtype Bad = Effect.Effect<number, Effect.Effect<string>, never>// ✅ Correct — use a plain error type in the failure channelclass MyError extends Data.TaggedError("MyError")<{ message: string }> {}type Good = Effect.Effect<number, MyError, never>
effectInVoidSuccess — ⚠️ warning
V3/V4: ✓ / ✓Detects nested Effect values in a void success channel. When a function returns void, any Effect returned inside it is silently discarded — it will never be executed, creating a hard-to-spot floating effect.
import { Effect } from "effect"// ❌ Violation — the inner Effect is silently discardedconst program: Effect.Effect<void> = Effect.gen(function* () { // This Effect is created but never yielded return Effect.log("this never runs")})// ✅ Correct — yield* the effect to execute itconst program = Effect.gen(function* () { yield* Effect.log("this runs")})
globalErrorInEffectCatch — ⚠️ warning
V3/V4: ✓ / ✓Warns when a catch callback (e.g., in Effect.tryPromise, Effect.try) returns the global Error type. Untagged errors merge together in the failure channel and lose type safety. Use a tagged error class instead.
import { Effect } from "effect"// ❌ Violation — global Error loses type informationconst result = Effect.tryPromise({ try: () => fetch("/api/users"), catch: (e) => new Error(String(e)) // global Error})// ✅ Correct — use a tagged error for type-safe error handlingclass FetchError extends Data.TaggedError("FetchError")<{ cause: unknown }> {}const result = Effect.tryPromise({ try: () => fetch("/api/users"), catch: (e) => new FetchError({ cause: e })})
globalErrorInEffectFailure — ⚠️ warning
V3/V4: ✓ / ✓Warns when the global Error type appears in the failure channel of an Effect type annotation. Like globalErrorInEffectCatch, untagged errors are indistinguishable from each other and cannot be handled precisely.
import { Effect, Data } from "effect"// ❌ Violation — global Error in the failure channeldeclare function loadConfig(): Effect.Effect<Config, Error>// ✅ Correct — use a discriminated tagged errorclass ConfigError extends Data.TaggedError("ConfigError")<{ message: string }> {}declare function loadConfig(): Effect.Effect<Config, ConfigError>
V3/V4: ✓ / ✓Detects when one layer inside a Layer.mergeAll call provides a service that another layer in the same call requires. Layer.mergeAll builds layers in parallel, so inter-layer dependencies are not satisfied. Move the dependent layer into a Layer.provideMerge call after the mergeAll.
import { Effect, Layer } from "effect"class Config extends Effect.Service<Config>()("Config", { effect: Effect.succeed({ url: "http://localhost" })}) {}class HttpClient extends Effect.Service<HttpClient>()("HttpClient", { effect: Effect.gen(function* () { const config = yield* Config return { get: () => Effect.void } })}) {}// ❌ Violation — HttpClient.Default requires Config, but both are in mergeAllconst AppLayer = Layer.mergeAll( Config.Default, HttpClient.Default // depends on Config, but mergeAll is parallel)// ✅ Correct — provide Config before mergingconst AppLayer = Layer.provideMerge(HttpClient.Default, Config.Default)
leakingRequirements — 💡 suggestion
V3/V4: ✓ / ✓Detects implementation services (internal dependencies) that are leaked through the public API of a service’s methods. When a service method requires an internal service from every caller, it exposes internal concerns to consumers. Resolve these dependencies at layer creation time instead.
V3/V4: ✓ / ✓Warns when Effect.provide is chained multiple times on the same effect. Chaining provide can cause service scope and lifecycle issues because each call creates a separate scope. Compose all layers into one and provide them in a single call.
import { Effect, Layer } from "effect"// ❌ Violation — multiple provide calls, separate scopesconst program = myEffect.pipe( Effect.provide(DatabaseLayer), Effect.provide(LoggerLayer), Effect.provide(ConfigLayer))// ✅ Correct — compose layers and provide onceconst AppLayer = Layer.mergeAll(DatabaseLayer, LoggerLayer, ConfigLayer)const program = myEffect.pipe(Effect.provide(AppLayer))
returnEffectInGen — 💡 suggestion + 🔧 quick fix
V3/V4: ✓ / ✓Warns when a return statement inside an Effect.gen generator returns an Effect-able value, resulting in a nested Effect<Effect<...>>. This is almost always unintentional — use return yield* to execute and unwrap the inner effect.
import { Effect } from "effect"// ❌ Violation — returns Effect<number>, producing Effect<Effect<number>>const program = Effect.gen(function* () { const id = yield* getUserId() return fetchUser(id) // Effect<User>, not User})// ✅ Correct — yield* the inner effect to get User directlyconst program = Effect.gen(function* () { const id = yield* getUserId() return yield* fetchUser(id)})
The quick fix adds yield* before the returned expression.
V3 only · V4: —Suggests using Runtime methods instead of calling Effect.runSync, Effect.runPromise, or similar functions inside an Effect context. Inside a generator, effects can simply be yielded. When you need to run a child effect using a specific runtime, use Effect.runtime and then the corresponding Runtime.* method.
import { Effect } from "effect"// ❌ Violation — Effect.runSync inside an Effect generatorconst program = Effect.gen(function* () { const result = Effect.runSync(someEffect) // anti-pattern return result})// ✅ Correct — yield* to execute inside the generatorconst program = Effect.gen(function* () { const result = yield* someEffect return result})
schemaSyncInEffect — 💡 suggestion (V3 only)
V3 only · V4: —Suggests using Effect-based Schema methods (e.g., Schema.decodeUnknown) instead of the synchronous variants (e.g., Schema.decodeUnknownSync) inside Effect generators. The sync methods throw on failure, bypassing Effect’s typed error channel. The Effect-based methods propagate errors through the channel with proper types.
import { Effect, Schema } from "effect"const UserSchema = Schema.Struct({ id: Schema.String, name: Schema.String })// ❌ Violation — sync method throws, bypassing the error channelconst program = Effect.gen(function* () { const user = Schema.decodeUnknownSync(UserSchema)(rawInput) // can throw return user})// ✅ Correct — Effect-based method surfaces errors in the error channelconst program = Effect.gen(function* () { const user = yield* Schema.decodeUnknown(UserSchema)(rawInput) return user})
V3 only · V4: —Suggests using Layer.scoped instead of Layer.effect when Scope appears in the layer’s requirements channel. Layer.scoped is the correct constructor for layers that manage scoped resources; using Layer.effect leaves Scope in the requirements, forcing callers to supply it.
import { Effect, Layer } from "effect"// ❌ Violation — Scope leaks into the layer requirementsconst ConnectionLayer = Layer.effect( Connection, Effect.gen(function* () { const scope = yield* Effect.scope // ... set up connection using scope return connection }))// ✅ Correct — Layer.scoped handles the Scope automaticallyconst ConnectionLayer = Layer.scoped( Connection, Effect.gen(function* () { const conn = yield* Effect.acquireRelease( openConnection(), (conn) => closeConnection(conn) ) return conn }))
strictEffectProvide — ➖ off by default
V3/V4: ✓ / ✓Warns when Effect.provide with a Layer is used outside of application entry points. Calling Effect.provide inside library code or service implementations can break scope lifetimes. All layers should be composed and provided once at the application entry point.This rule is off by default because there are valid use cases for Effect.provide in non-entry-point code (for example, test helpers). Enable it when you want to enforce strict layer discipline:
import { Effect, Layer } from "effect"// ❌ Violation (when enabled) — provide inside a service methodconst makeUser = Effect.gen(function* () { const db = yield* Database return db.createUser()}).pipe(Effect.provide(Database.Default))// ✅ Correct — provide at the application entry pointconst program = makeUser.pipe( Effect.provide(AppLayer))Effect.runPromise(program)
tryCatchInEffectGen — 💡 suggestion
V3/V4: ✓ / ✓Discourages the use of try/catch inside Effect.gen generators. Exception-based error handling bypasses Effect’s typed error channel, making errors invisible to the type system. Use Effect.try, Effect.tryPromise, Effect.catch, or Effect.catchTag instead.
import { Effect } from "effect"// ❌ Violation — try/catch bypasses the typed error channelconst program = Effect.gen(function* () { try { const result = yield* riskyEffect return result } catch (e) { return defaultValue // error is untyped }})// ✅ Correct — use Effect's error handling mechanismsconst program = riskyEffect.pipe( Effect.catchAll(() => Effect.succeed(defaultValue)))
unknownInEffectCatch — ⚠️ warning
V3/V4: ✓ / ✓Warns when a catch callback (e.g., in Effect.tryPromise, Effect.try) returns unknown. An unknown error type is too wide to handle precisely. Narrow the type or wrap the error in a tagged class to make it useful in the error channel.
import { Effect, Data } from "effect"// ❌ Violation — catch returns unknownconst result = Effect.tryPromise({ try: () => fetch("/api"), catch: (e) => e // type is unknown})// ✅ Correct — wrap in a tagged error with contextclass FetchError extends Data.TaggedError("FetchError")<{ cause: unknown }> {}const result = Effect.tryPromise({ try: () => fetch("/api"), catch: (e) => new FetchError({ cause: e })})