Problem
yield* Effect.sleep(0) (or Effect.sleep("0 millis"), Effect.sleep(Duration.zero)) is the carried-over setTimeout(0) idiom: the author wants to give other fibers a chance to run before continuing. Effect has a dedicated primitive for exactly that intent — Effect.yieldNow (an Effect<void> constant in v4) — and the two are not equivalent. Effect.sleep schedules the fiber through the Clock service: it registers a timer, which means it also interacts with TestClock — under test, a sleep(0) still needs the test clock to advance, whereas the author almost certainly just wanted a cooperative yield. Effect.yieldNow simply reschedules the fiber on the scheduler with no Clock involvement, which is what a zero-duration sleep is trying to express. So this is a prefer-this-API rule with a real semantic payoff: clearer intent, no timer machinery, and no surprising TestClock interaction. Related: #477.
Bad — compiles cleanly, the rule should flag this
// RULE: sleepZeroToYieldNow
// BAD: Effect.sleep with a zero duration goes through the Clock service —
// it registers a timer (and under TestClock waits for the clock to advance)
// just to let other fibers run. The intent here is a cooperative yield, not
// a timed sleep.
import { Duration, Effect } from "effect"
const processChunk = (chunk: ReadonlyArray<number>) =>
Effect.sync(() => chunk.reduce((acc, n) => acc + n, 0))
const processAll = (chunks: ReadonlyArray<ReadonlyArray<number>>) =>
Effect.gen(function*() {
let total = 0
for (const chunk of chunks) {
total += yield* processChunk(chunk)
// "let other fibers breathe" — but this is a Clock-backed timer
yield* Effect.sleep(0)
// equivalent spellings the rule should also catch:
// yield* Effect.sleep("0 millis")
// yield* Effect.sleep(Duration.zero)
}
return total
})
Good
// RULE: sleepZeroToYieldNow
// GOOD: Effect.yieldNow reschedules the fiber on the scheduler directly —
// no Clock, no timer, no TestClock interaction. It states the intent
// ("yield to other fibers") exactly.
import { Effect } from "effect"
const processChunk = (chunk: ReadonlyArray<number>) =>
Effect.sync(() => chunk.reduce((acc, n) => acc + n, 0))
const processAll = (chunks: ReadonlyArray<ReadonlyArray<number>>) =>
Effect.gen(function*() {
let total = 0
for (const chunk of chunks) {
total += yield* processChunk(chunk)
yield* Effect.yieldNow
}
return total
})
Proposed rule behavior
- Match
CallExpression whose callee resolves (via checker symbol resolution) to Effect.sleep from the effect package.
- Flag when the single argument is provably zero: the numeric literal
0, a string literal matching a zero-duration form ("0 nanos", "0 micros", "0 millis", "0 seconds", etc.), or a property access resolving to Duration.zero.
- Optionally also cover constructor calls that are statically zero, e.g.
Duration.millis(0) / Duration.seconds(0), via the same symbol check plus a literal-argument check.
- Report with a suggested rewrite to
Effect.yieldNow, and word the message around the semantic difference: sleep(0) schedules via the Clock (and interacts with TestClock), yieldNow just reschedules the fiber.
- Do not flag non-literal arguments (variables, config-derived durations) — a runtime-zero duration may be intentional Clock behavior, and the rule should stay effectively false-positive-free by only matching provably-zero literals.
- Grounding note: the only textual match in the reference codebases is the JSDoc doctest inside
Effect.sleep's own documentation, so the rule should run on regular source (doctest extraction would self-flag the docs example).
Where this came up
No true-positive occurrences found in Effect-TS/effect@c3c7647 or anomalyco/opencode@550d1ff — proposed from the API sweep; both reference codebases are expert-written, so absence there is weak negative signal.
Mined from a per-export sweep of the Effect module (v4): for each exported function, asking what manual pattern it replaces and whether that pattern is statically detectable; grounded against Effect-TS/effect and anomalyco/opencode; deduplicated against implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
sleepZeroToYieldNow
Incremental true-positive recount: T3 Code
Reviewed pingdotgg/t3code at 01e05c15268d on 2026-09-14. Scope: tracked first-party TypeScript/JavaScript, including authored tests unless excluded by this proposal; vendored .repos, generated files, dependencies, build output and documentation examples excluded.
- New T3 Code matches: 0. Counts refer to vetted diagnostic source sites, not observed production failures.
- Previous reviewed count bucket:
value:tp-0.
- Confirmed aggregate minimum: 0; label:
value:tp-0. Previously vetted sites remain included; this pass only adds T3 Code.
Review notes. All first-party Effect.sleep arguments were reviewed: nonzero literals or runtime durations; no provably-zero literal/Duration.zero/zero constructor.
No new source location met the reviewed trigger and exclusions. Uncertain and version-inapplicable candidates were not added to the count.
Problem
yield* Effect.sleep(0)(orEffect.sleep("0 millis"),Effect.sleep(Duration.zero)) is the carried-oversetTimeout(0)idiom: the author wants to give other fibers a chance to run before continuing. Effect has a dedicated primitive for exactly that intent —Effect.yieldNow(anEffect<void>constant in v4) — and the two are not equivalent.Effect.sleepschedules the fiber through theClockservice: it registers a timer, which means it also interacts withTestClock— under test, asleep(0)still needs the test clock to advance, whereas the author almost certainly just wanted a cooperative yield.Effect.yieldNowsimply reschedules the fiber on the scheduler with no Clock involvement, which is what a zero-duration sleep is trying to express. So this is a prefer-this-API rule with a real semantic payoff: clearer intent, no timer machinery, and no surprising TestClock interaction. Related: #477.Bad — compiles cleanly, the rule should flag this
Good
Proposed rule behavior
CallExpressionwhose callee resolves (via checker symbol resolution) toEffect.sleepfrom theeffectpackage.0, a string literal matching a zero-duration form ("0 nanos","0 micros","0 millis","0 seconds", etc.), or a property access resolving toDuration.zero.Duration.millis(0)/Duration.seconds(0), via the same symbol check plus a literal-argument check.Effect.yieldNow, and word the message around the semantic difference:sleep(0)schedules via the Clock (and interacts withTestClock),yieldNowjust reschedules the fiber.Effect.sleep's own documentation, so the rule should run on regular source (doctest extraction would self-flag the docs example).Where this came up
No true-positive occurrences found in Effect-TS/effect@c3c7647 or anomalyco/opencode@550d1ff — proposed from the API sweep; both reference codebases are expert-written, so absence there is weak negative signal.
Mined from a per-export sweep of the Effect module (v4): for each exported function, asking what manual pattern it replaces and whether that pattern is statically detectable; grounded against Effect-TS/effect and anomalyco/opencode; deduplicated against implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
sleepZeroToYieldNowIncremental true-positive recount: T3 Code
Reviewed pingdotgg/t3code at
01e05c15268don 2026-09-14. Scope: tracked first-party TypeScript/JavaScript, including authored tests unless excluded by this proposal; vendored.repos, generated files, dependencies, build output and documentation examples excluded.value:tp-0.value:tp-0. Previously vetted sites remain included; this pass only adds T3 Code.Review notes. All first-party Effect.sleep arguments were reviewed: nonzero literals or runtime durations; no provably-zero literal/Duration.zero/zero constructor.
No new source location met the reviewed trigger and exclusions. Uncertain and version-inapplicable candidates were not added to the count.