Skip to content

fix(Effect): handle transpiled generator bodies in Effect.fn#6254

Open
markgdawson wants to merge 1 commit into
Effect-TS:mainfrom
markgdawson:fix/fn-iterator-result
Open

fix(Effect): handle transpiled generator bodies in Effect.fn#6254
markgdawson wants to merge 1 commit into
Effect-TS:mainfrom
markgdawson:fix/fn-iterator-result

Conversation

@markgdawson
Copy link
Copy Markdown

Summary

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 non-generator function returning an iterator. For example, babel-preset-expo on React Native / Hermes rewrites destructured-param generators like this:

// Source:
function*({ a }) { return a }

// babel-preset-expo output (approx):
function destr(_ref) {
  var a = _ref.a;
  return (function*() { return a })();
}

The lowered wrapper is no longer a GeneratorFunction, so isGeneratorFunction(body) in fnApply returns false. The else branch then takes body.apply(...)'s return value — an Iterator, not an Effect — and passes it to withSpan, which hands it to the fiber runLoop. The loop dies at the first instruction because the value has no _op.

Repro

import { Effect } from "effect"

// Simulates a transpiler lowering `function*({a}) { return a }`
const body = function (arg: { a: number }) {
  const a = arg.a
  return (function* () { return a })()
}

const fn = Effect.fn("repro")(body as any)
await Effect.runPromise(fn({ a: 42 }))
// 💥 RuntimeException: Not a valid effect: {}

Root cause

isGeneratorFunction in Utils.ts checks u.constructor === function*(){}.constructor. That works for native generators and for transpilers that preserve function* syntax in their output, but fails for any transformer that lowers function* into a plain function — including Babel's @babel/plugin-transform-parameters, which babel-preset-expo ships unconditionally on Hermes-native.

Effect.fn has had this hazard since it was introduced in 3.11.0. It is not a regression — it has always crashed under transformers that erase function* syntax.

Fix

In fnApply's else branch, after calling body.apply(...), duck-type the result. If it looks like an iterator (has .next and Symbol.iterator) and isn't already an Effect, 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 does not need this change: it unconditionally wraps with fromIterator, so a transpiler-lowered body's returned iterator flows through correctly already.

Test

Added a regression test in packages/effect/test/Effect/fn.test.ts constructing the exact shape the transpiler produces — a plain function returning a generator IIFE. Verified red → green against main.

Related

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.
@markgdawson markgdawson requested a review from mikearnaldi as a code owner June 3, 2026 16:49
@github-project-automation github-project-automation Bot moved this to Discussion Ongoing in PR Backlog Jun 3, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 3, 2026

🦋 Changeset detected

Latest commit: 36266c6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
effect Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Discussion Ongoing

Development

Successfully merging this pull request may close these issues.

2 participants