Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Effect-TS/tsgo/llms.txt

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.
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 unreachable
const result = Effect.succeed(42).pipe(
  Effect.catchAll((e) => Effect.succeed(0)) // never triggered
)

// ✅ Correct — only add error handling when the effect can fail
const result = Effect.succeed(42)
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 reused
const result = Effect.fn("doWork")(function* () {
  return yield* Effect.succeed(42)
})()

// ✅ Correct — use Effect.gen when you don't need a reusable function
const result = Effect.gen(function* () {
  return yield* Effect.succeed(42)
})
The quick fix replaces the Effect.fn(...)() call with Effect.gen.
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 needed
const program = Effect.gen(function* (_) {
  const value = yield* _(Effect.succeed(42))
  return value
})

// ✅ Correct — drop the adapter and use yield* directly
const program = Effect.gen(function* () {
  const value = yield* Effect.succeed(42)
  return value
})
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 executed
type Bad = Effect.Effect<number, Effect.Effect<string>, never>

// ✅ Correct — use a plain error type in the failure channel
class MyError extends Data.TaggedError("MyError")<{ message: string }> {}
type Good = Effect.Effect<number, MyError, never>
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 discarded
const 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 it
const program = Effect.gen(function* () {
  yield* Effect.log("this runs")
})
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 information
const result = Effect.tryPromise({
  try: () => fetch("/api/users"),
  catch: (e) => new Error(String(e)) // global Error
})

// ✅ Correct — use a tagged error for type-safe error handling
class FetchError extends Data.TaggedError("FetchError")<{ cause: unknown }> {}

const result = Effect.tryPromise({
  try: () => fetch("/api/users"),
  catch: (e) => new FetchError({ cause: e })
})
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 channel
declare function loadConfig(): Effect.Effect<Config, Error>

// ✅ Correct — use a discriminated tagged error
class 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 mergeAll
const AppLayer = Layer.mergeAll(
  Config.Default,
  HttpClient.Default // depends on Config, but mergeAll is parallel
)

// ✅ Correct — provide Config before merging
const AppLayer = Layer.provideMerge(HttpClient.Default, Config.Default)
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.
import { Effect } from "effect"

class Logger extends Effect.Service<Logger>()("Logger", {
  effect: Effect.succeed({ log: (msg: string) => Effect.log(msg) })
}) {}

// ❌ Violation — Logger leaks into callers of UserService
class UserService extends Effect.Service<UserService>()("UserService", {
  effect: Effect.succeed({
    getUser: (id: string) =>
      Effect.gen(function* () {
        const logger = yield* Logger // requirement leaks to callers
        yield* logger.log(`getting user ${id}`)
        return { id, name: "Alice" }
      })
  })
}) {}

// ✅ Correct — resolve Logger inside the layer
class UserService extends Effect.Service<UserService>()("UserService", {
  effect: Effect.gen(function* () {
    const logger = yield* Logger
    return {
      getUser: (id: string) =>
        Effect.gen(function* () {
          yield* logger.log(`getting user ${id}`)
          return { id, name: "Alice" }
        })
    }
  })
}) {}
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 scopes
const program = myEffect.pipe(
  Effect.provide(DatabaseLayer),
  Effect.provide(LoggerLayer),
  Effect.provide(ConfigLayer)
)

// ✅ Correct — compose layers and provide once
const AppLayer = Layer.mergeAll(DatabaseLayer, LoggerLayer, ConfigLayer)
const program = myEffect.pipe(Effect.provide(AppLayer))
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 directly
const 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 generator
const program = Effect.gen(function* () {
  const result = Effect.runSync(someEffect) // anti-pattern
  return result
})

// ✅ Correct — yield* to execute inside the generator
const program = Effect.gen(function* () {
  const result = yield* someEffect
  return result
})
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 channel
const program = Effect.gen(function* () {
  const user = Schema.decodeUnknownSync(UserSchema)(rawInput) // can throw
  return user
})

// ✅ Correct — Effect-based method surfaces errors in the error channel
const 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 requirements
const 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 automatically
const ConnectionLayer = Layer.scoped(
  Connection,
  Effect.gen(function* () {
    const conn = yield* Effect.acquireRelease(
      openConnection(),
      (conn) => closeConnection(conn)
    )
    return conn
  })
)
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:
{ "diagnosticSeverity": { "strictEffectProvide": "warning" } }
import { Effect, Layer } from "effect"

// ❌ Violation (when enabled) — provide inside a service method
const makeUser = Effect.gen(function* () {
  const db = yield* Database
  return db.createUser()
}).pipe(Effect.provide(Database.Default))

// ✅ Correct — provide at the application entry point
const program = makeUser.pipe(
  Effect.provide(AppLayer)
)
Effect.runPromise(program)
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 channel
const program = Effect.gen(function* () {
  try {
    const result = yield* riskyEffect
    return result
  } catch (e) {
    return defaultValue // error is untyped
  }
})

// ✅ Correct — use Effect's error handling mechanisms
const program = riskyEffect.pipe(
  Effect.catchAll(() => Effect.succeed(defaultValue))
)
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 unknown
const result = Effect.tryPromise({
  try: () => fetch("/api"),
  catch: (e) => e // type is unknown
})

// ✅ Correct — wrap in a tagged error with context
class FetchError extends Data.TaggedError("FetchError")<{ cause: unknown }> {}

const result = Effect.tryPromise({
  try: () => fetch("/api"),
  catch: (e) => new FetchError({ cause: e })
})

Build docs developers (and LLMs) love