From 36266c607900b8fe096e8b122d485cd8c1d63998 Mon Sep 17 00:00:00 2001 From: Mark Dawson Date: Wed, 3 Jun 2026 17:47:07 +0100 Subject: [PATCH] fix(Effect): handle transpiled generator bodies in Effect.fn Effect.fn(name)(body) crashes at runtime with "RuntimeException: Not a valid effect: {}" when body is a generator function that has been lowered by a bundler/transpiler into a plain function returning an iterator IIFE (e.g. babel-preset-expo on React Native / Hermes). isGeneratorFunction returns false for such bodies, so the iterator was passed through as if it were an Effect. Detect the iterator-shape post-apply and re-wrap it with core.fromIterator. The first iterator is consumed immediately; subsequent invocations re-apply body to produce fresh iterators, preserving Effect.fn's reusable-wrapper contract. fnUntraced is unaffected: it always wraps with fromIterator already. --- .changeset/fix-fn-iterator-result.md | 7 +++++++ packages/effect/src/Effect.ts | 25 ++++++++++++++++++++++++- packages/effect/test/Effect/fn.test.ts | 17 +++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-fn-iterator-result.md diff --git a/.changeset/fix-fn-iterator-result.md b/.changeset/fix-fn-iterator-result.md new file mode 100644 index 00000000000..162bcfd388a --- /dev/null +++ b/.changeset/fix-fn-iterator-result.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +fix(Effect): handle transpiled generator bodies in `Effect.fn` + +`Effect.fn(name)(body)` crashed at runtime with `RuntimeException: Not a valid effect: {}` when `body` was a generator function lowered by a compiler (e.g. `babel-preset-expo` on React Native / Hermes) into a plain function returning an iterator IIFE. Such a body fails the `isGeneratorFunction` check, so its return value was passed through as if it were an `Effect`. We now duck-type the result and re-wrap it with `fromIterator` when it is an iterator. diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index d9143c239b6..52136152c41 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -14707,7 +14707,30 @@ function fnApply(options: { effect = core.fromIterator(() => options.body.apply(options.self, options.args)) } else { try { - effect = options.body.apply(options.self, options.args) + const result = options.body.apply(options.self, options.args) + // Some compilers (e.g. `babel-preset-expo` on React Native / Hermes) + // lower destructured-param generators into a plain function that returns + // an iterator IIFE. Such a body fails `isGeneratorFunction` but its + // return value is an iterator, not an Effect. Detect and re-wrap. + if ( + result !== null && + typeof result === "object" && + (result as any)[EffectTypeId] === undefined && + typeof (result as any).next === "function" && + typeof (result as any)[Symbol.iterator] === "function" + ) { + let firstIterator: any = result + effect = core.fromIterator(() => { + if (firstIterator !== null) { + const it = firstIterator + firstIterator = null + return it + } + return options.body.apply(options.self, options.args) + }) + } else { + effect = result + } } catch (error) { fnError = error effect = die(error) diff --git a/packages/effect/test/Effect/fn.test.ts b/packages/effect/test/Effect/fn.test.ts index a5416625a5a..2bc053b5d18 100644 --- a/packages/effect/test/Effect/fn.test.ts +++ b/packages/effect/test/Effect/fn.test.ts @@ -76,6 +76,23 @@ describe("Effect.fn", () => { strictEqual(fn2.length, 1) strictEqual(Effect.runSync(fn2(2)), 2) }) + + it.effect("handles a non-generator body that returns an iterator (transpiled generator)", () => + Effect.gen(function*() { + // Mimics `babel-preset-expo` lowering `function*({a}) { return a }` into + // a plain function that returns a generator IIFE iterator. + const body = function(arg: { a: number }) { + const a = arg.a + return (function*() { + return a + })() + } + const fn = Effect.fn("test")(body as any) + const v = yield* fn({ a: 42 }) as Effect.Effect + strictEqual(v, 42) + const v2 = yield* fn({ a: 7 }) as Effect.Effect + strictEqual(v2, 7) + })) }) describe("Effect.fnUntraced", () => {