Skip to content

Fix runEffectInsideEffect for Exit runners and Effect.context, update Effect v4 to 4.0.5 - #781

Merged
mattiamanzati merged 6 commits into
mainfrom
fix/run-effect-inside-effect-exit
Oct 16, 2026
Merged

mattiamanzati merged 6 commits into
mainfrom
fix/run-effect-inside-effect-exit

Conversation

@mattiamanzati

Copy link
Copy Markdown
Contributor

Closes #779
Closes #780

Summary

This updates Effect v4 support from 4.0.0-rc.118 to 4.0.5 and fixes two bugs in runEffectInsideEffect.

Exit runners are reported (#780)

Effect.runPromiseExit and Effect.runSyncExit start a separate fiber, just like runPromise and runSync. The rule only matched four runner names, so the Exit forms were never reported. They now get the same diagnostic and quick fix.

export const nested = Effect.gen(function*() {
  yield* Effect.void
  setTimeout(() => {
    Effect.runPromiseExit(child)
    // ^- `Effect.runPromiseExit` is called inside an Effect with a separate services invocation.
    //    In this context, child Effects run with the surrounding services, which can be accessed
    //    through `Effect.context` and `Effect.runPromiseExitWith`.
  }, 0)
})

In v4 the quick fix rewrites the call to Effect.runPromiseExitWith(effectContext)(child). In v3 it rewrites it to Runtime.runPromiseExit(effectRuntime, child).

The v4 quick fix uses Effect.context (#779)

Effect 4 exports Effect.context, not Effect.services. The quick fix still inserted yield* Effect.services<never>(), so the fixed file no longer typechecked. It also looked for an existing services binding, so an existing context binding was never reused.

The fix now inserts yield* Effect.context<never>() and reuses an existing binding:

// before the fix
export const program = Effect.gen(function*() {
  const context = yield* Effect.context<never>()
  const run = () => Effect.runSync(Effect.succeed(1))
  return [context, run()] as const
})

// after applying "Use the current services to run the Effect"
export const program = Effect.gen(function*() {
  const context = yield* Effect.context<never>()
  const run = () => Effect.runSyncWith(context)(Effect.succeed(1))
  return [context, run()] as const
})

Test harness

The v4 suite type-checks each example and each quick-fix output, but it filtered TypeScript diagnostics on _.source. Compiler diagnostics have no source, so every error was dropped. That is why #779 went unnoticed. The filter now matches on _.file?.fileName. Two other changes were needed so the check works:

  • The mock language service host now implements getDirectories and sets typeRoots to the harness node_modules/@types, so @types/node resolves. Without this, the nodeBuiltinImport examples fail with Cannot find module 'fs'.
  • Chained diagnostic messages are flattened, so they no longer print as [object Object].

With the filter fixed, two unrelated quick fixes turn out to produce code that does not typecheck. Their snapshots now show the errors, and they are not fixed in this PR:

  • effectFnOpportunity_plainMany.ts, on a generic function that returns a union of Effects, fails with "No overload matches this call".
  • overriddenSchemaConstructor_static.ts and overriddenSchemaConstructor_preview.ts fail with "Property 'b' has no initializer and is not definitely assigned in the constructor".

Effect 4.0.5

The version bump touches only package.json files and the lockfile. It changed no snapshots in the v4 suite.

Commits

The commits are ordered so each failing state lands before its fix:

  1. Add a repro example for the Exit runners, with snapshots that show them unreported.
  2. Update Effect v4 to 4.0.5.
  3. Report TypeScript errors in v4 diagnostic snapshots. The runEffectInsideEffect fix outputs now show the Property 'services' does not exist error from runEffectInsideEffect: v4 quick fix still emits Effect.services, so the fixed code fails to typecheck on effect 4.x #779.
  4. Use Effect.context in the v4 quick fix (runEffectInsideEffect: v4 quick fix still emits Effect.services, so the fixed code fails to typecheck on effect 4.x #779).
  5. Report the Exit runners (runEffectInsideEffect: Effect.runPromiseExit and Effect.runSyncExit inside an Effect are not reported, unlike runPromise and runSync #780).
  6. Add a changeset.

Verification

  • pnpm check and pnpm lint-fix pass.
  • pnpm test:v4: 657 passed, 3 skipped.
  • pnpm test:v3: 653 passed, 2 skipped.

🤖 Generated with Claude Code

mattiamanzati and others added 6 commits October 16, 2026 16:48
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compiler diagnostics have no `source`, so filtering on it dropped every
error from the v4 initial-code and quick-fix output checks. Filter on the
diagnostic file instead, flatten chained messages, and let the mock host
enumerate the harness type roots so @types/node resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect 4 exports Effect.context, not Effect.services, so the inserted
binding failed to typecheck and an existing context binding was never
reused.

Fixes #779

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect.runPromiseExit and Effect.runSyncExit start a separate fiber just
like their non-Exit forms, so they now get the same diagnostic and fix.

Fixes #780

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 913a497

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

This PR includes changesets to release 1 package
Name Type
@effect/language-service 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

@mattiamanzati
mattiamanzati merged commit dae2015 into main Oct 16, 2026
5 checks passed
@mattiamanzati
mattiamanzati deleted the fix/run-effect-inside-effect-exit branch October 16, 2026 17:09
@github-actions github-actions Bot mentioned this pull request Oct 16, 2026
mattiamanzati added a commit to Effect-TS/tsgo that referenced this pull request Oct 19, 2026
…romiseExit (#876)

* test(runEffectInsideEffect): add Exit runner and existing context examples

Ports runEffectInsideEffect_exit.ts (v3, v4) and
runEffectInsideEffect_existingContext.ts (v4) from
Effect-TS/language-service#781. The baselines record current behavior:
Effect.runSyncExit and Effect.runPromiseExit are not reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(runEffectInsideEffect): report Effect.runSyncExit and Effect.runPromiseExit

The Exit runners start a separate fiber just like runSync and
runPromise, but the rule only matched four runner names. Port of
Effect-TS/language-service#781.

Fixes #873

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant