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
@effect/vitest could flush the scheduler after each test before returning control to vitest
- Use
queueMicrotask instead of Promise.resolve().then() — microtasks don't create trackable async resources in node:async_hooks
- 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.
Description
When using
@effect/vitestwith vitest'sdetectAsyncLeaks: true, everyit.effect()/it.scoped()test reports false positive PROMISE leaks from Effect'sMixedScheduler.Reproduction
Output:
Stack trace:
Root Cause
MixedSchedulerusesPromise.resolve(void 0).then(() => drain(depth + 1))for fiber scheduling (Scheduler.ts#L131). Vitest tracks every PROMISE async resource vianode:async_hooksand only removes them onpromiseResolve. The scheduler's intermediate promises haven't resolved by the time vitest collects leaks.What We Tried
it.scoped()Effect.scopedpipeafterAllwith 200ms delaySyncSchedulerviaEffect.locallyMixedScheduler(0)(force setTimeout)Timeoutinstead ofPROMISELayer.locallyScoped(currentScheduler, SyncScheduler)Expected Behavior
@effect/vitesttest helpers should properly clean up MixedScheduler resources so thatdetectAsyncLeaksdoesn't report false positives.Possible Solutions
@effect/vitestcould flush the scheduler after each test before returning control to vitestqueueMicrotaskinstead ofPromise.resolve().then()— microtasks don't create trackable async resources innode:async_hooksEnvironment
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
detectAsyncLeaksto catch real leaks.