Skip to content

detectAsyncLeaks false positive: MixedScheduler PROMISE leaks with @effect/vitest #6126

@gabriel-ecegi

Description

@gabriel-ecegi

Description

When using @effect/vitest with vitest's detectAsyncLeaks: true, every it.effect() / it.scoped() test reports false positive PROMISE leaks from Effect's MixedScheduler.

Reproduction

// vitest.config.ts
export default defineConfig({
  test: { detectAsyncLeaks: true }
});

// example.test.ts
import { it, describe, expect } from "@effect/vitest";
import { Effect } from "effect";

describe("leak demo", () => {
  it.effect("simple effect leaks PROMISE", () =>
    Effect.gen(function* () {
      expect(1).toBe(1);
    })
  );
});

Output:

⎯⎯⎯ Async Leaks 2 ⎯⎯⎯
PROMISE leaking in example.test.ts

Stack trace:

MixedScheduler.starveInternal Scheduler.js:68:17
Scheduler.js:84:47

Root Cause

MixedScheduler uses Promise.resolve(void 0).then(() => drain(depth + 1)) for fiber scheduling (Scheduler.ts#L131). Vitest tracks every PROMISE async resource via node:async_hooks and only removes them on promiseResolve. The scheduler's intermediate promises haven't resolved by the time vitest collects leaks.

What We Tried

Approach Result
it.scoped() Still leaks — scheduler is outside scope
Effect.scoped pipe Same
afterAll with 200ms delay Doesn't help — vitest measures per-test
SyncScheduler via Effect.locally Deadlocks on any async operation (fetch)
MixedScheduler(0) (force setTimeout) Leaks Timeout instead of PROMISE
Layer.locallyScoped(currentScheduler, SyncScheduler) Hangs with scoped effects

Expected Behavior

@effect/vitest test helpers should properly clean up MixedScheduler resources so that detectAsyncLeaks doesn't report false positives.

Possible Solutions

  1. @effect/vitest could flush the scheduler after each test before returning control to vitest
  2. Use queueMicrotask instead of Promise.resolve().then() — microtasks don't create trackable async resources in node:async_hooks
  3. Document the incompatibility if it can't be fixed

Environment

  • effect: 3.19.19
  • @effect/vitest: 0.27.0
  • vitest: 4.1.0
  • bun: 1.3.10
  • pool: forks

Note

Vitest's leak detection does NOT fail tests (exit code 0) — leaks are warnings only. But they pollute CI output and prevent teams from using detectAsyncLeaks to catch real leaks.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions