Common Mistakes

Mistakes you’ll make when learning Effect, and how to fix them.

try-catch Inside Effect.gen

// WRONG - breaks Effect semantics, catch block never fires
Effect.gen(function*() {
  try {
    const result = yield* someEffect
    return result
  } catch (error) {
    console.error(error)
  }
})

// CORRECT - use Effect's error channel
Effect.gen(function*() {
  const result = yield* Effect.either(someEffect)
  if (result._tag === "Left") {
    console.error("Effect failed:", result.left)
    return yield* Effect.fail("Handled error")
  }
  return result.right
})

Effect handles errors through the E channel, not JavaScript exceptions. A try-catch block inside a generator will never catch an Effect failure because yield* short-circuits on errors before the exception mechanism sees them.

Missing return Before yield* for Errors

// WRONG - code after yield* Effect.fail is unreachable, TypeScript can't tell
Effect.gen(function*() {
  if (invalidCondition) {
    yield* Effect.fail("Validation failed")
  }
  const result = yield* nextStep // unreachable but TypeScript doesn't know
  return result
})

// CORRECT - return makes termination explicit
Effect.gen(function*() {
  if (invalidCondition) {
    return yield* Effect.fail("Validation failed")
  }
  const result = yield* nextStep
  return result
})

The return keyword tells TypeScript that the branch terminates. Without it, TypeScript thinks execution continues after Effect.fail, leading to type errors or misleading unreachable code warnings.

Gotcha: Always use return yield* for Effect.fail, Effect.interrupt, Effect.die, and any tagged error thrown with return yield* new MyError(...).

Using .pipe with Effect.fn

// WRONG - .pipe on Effect.fn breaks tracing and composition
const fn = Effect.fn("myFunction")(
  function*(n: number) {
    return n + 1
  }
).pipe(
  Effect.catch((error) => Effect.logError(error))
)

// CORRECT - pass combinators as additional arguments
const fn = Effect.fn("myFunction")(
  function*(n: number) {
    return n + 1
  },
  Effect.catch((error) => Effect.logError(error))
)

Effect.fn accepts combinators as trailing arguments. Using .pipe on the result defeats the purpose: it breaks span tracing and loses the function name from stack traces.

Wrong Layer Composition Order

// WRONG - providing in the wrong direction
const main = program.pipe(
  Effect.provide(Layer.provide(AppLayer, DatabaseLayer))
  // AppLayer depends on DatabaseLayer, but this is backwards
)

// CORRECT - Layer.provide(dependencyLayer, dependentLayer)
// The layer being provided comes first
const main = program.pipe(
  Effect.provide(
    Layer.provide(DatabaseLayer, AppLayer)
  )
)

Layer.provide(A, B) means “provide A to B.” The first argument is the dependency, the second is the consumer. If you get the direction wrong, you’ll get a type error about unsatisfied requirements on the R channel.

Tip: Think of it as “feed A into B.” A is what B needs.

Missing Service Dependencies

// Error at runtime: "Context Service not found: myapp/Database"
const program = Effect.gen(function*() {
  const db = yield* Database
  return yield* db.query("SELECT * FROM users")
})

Effect.runPromise(program) // crashes - no Database layer provided

// Fix: provide the layer
Effect.runPromise(
  program.pipe(Effect.provide(Database.layer))
)

Every service you yield* must have its layer provided to the runtime. The error message includes the service identifier string, which tells you exactly which layer is missing.

Gotcha: The error happens at runtime, not compile time, because Effect’s dependency injection is structurally typed. The R channel tracks requirements at the type level, but Effect.runPromise accepts Effect<unknown, unknown, never> and won’t catch missing layers at compile time.

Using v3 Subtyping Patterns in v4

// v3 - Ref, Deferred, Fiber were Effect subtypes
const ref = yield* Ref.make(0)
const value = yield* ref // worked in v3

// v4 - Ref is a plain value, not an Effect
const ref = yield* Ref.make(0)
const value = yield* Ref.get(ref) // explicit method call
v3 Patternv4 Replacement
yield* refyield* Ref.get(ref)
yield* deferredyield* Deferred.await(deferred)
yield* fiberyield* Fiber.join(fiber)
Effect.map(option, fn)option.asEffect().pipe(Effect.map(fn)) or yield* in generator

See v3 to v4 Migration for the full Yieldable trait changes.

Using catchAll Instead of catch

// v3
Effect.fail("error").pipe(
  Effect.catchAll((error) => Effect.succeed("recovered"))
)

// v4 - renamed to catch
Effect.fail("error").pipe(
  Effect.catch((error) => Effect.succeed("recovered"))
)
v3v4
catchAllcatch
catchAllCausecatchCause
catchAllDefectcatchDefect
catchSomecatchFilter
catchSomeCausecatchCauseFilter
catchTagcatchTag (unchanged)
catchTagscatchTags (unchanged)
catchIfcatchIf (unchanged)

Not Providing All Required Layers

// Type error: Effect<Users, DatabaseError, Database | Config>
// R channel still has Config - you forgot to provide it
const program = Effect.gen(function*() {
  const db = yield* Database
  const config = yield* Config
  return yield* db.query(config.query)
})

// Only provided Database, Config is still required
program.pipe(Effect.provide(Database.layer)) // type error on R channel

// Fix: provide both, or compose layers
program.pipe(
  Effect.provide(
    Layer.mergeAll(Database.layer, Config.layer)
  )
)

The R channel in Effect<A, E, R> tracks remaining dependencies. If R is not never after all .provide calls, you have unmet dependencies. TypeScript will flag this as a type error when you try to run the effect.

Tip: Compose layers into a single Main layer at your application entry point. Then every program only needs one Effect.provide(Main).