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*forEffect.fail,Effect.interrupt,Effect.die, and any tagged error thrown withreturn 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
Rchannel tracks requirements at the type level, butEffect.runPromiseacceptsEffect<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 Pattern | v4 Replacement |
|---|---|
yield* ref | yield* Ref.get(ref) |
yield* deferred | yield* Deferred.await(deferred) |
yield* fiber | yield* 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"))
)
| v3 | v4 |
|---|---|
catchAll | catch |
catchAllCause | catchCause |
catchAllDefect | catchDefect |
catchSome | catchFilter |
catchSomeCause | catchCauseFilter |
catchTag | catchTag (unchanged) |
catchTags | catchTags (unchanged) |
catchIf | catchIf (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
Mainlayer at your application entry point. Then every program only needs oneEffect.provide(Main).