Problem
Inside Effect.callback((resume) => { ... }), flags bodies where the resume parameter is never invoked (or never referenced) on any code path, or is only invoked in an error branch while the success branch never resumes. The types are satisfied regardless of whether resume is called, but the resulting effect can only complete via resume.
Why the compiler is silent / what breaks at runtime: The effect type-checks with any success/error type you claim, but a callback that never calls resume produces a fiber that suspends forever — the program silently hangs until externally interrupted.
Both examples below type-check with zero errors against effect@4.0.0-beta.104 (re-verified with an isolated strict tsconfig) — so the compiler offers no protection here and a diagnostic is the only static safety net.
Bad — compiles cleanly, the rule should flag this
// RULE: effect-callback-never-resumes
// BAD: the Effect.callback bodies below type-check for any claimed success/error
// type, but the fiber can only ever complete via `resume`. Variant 1 never
// references resume at all; variant 2 resumes only on the error path, so a
// successful `listening` event leaves the fiber suspended forever. Both hang
// silently until externally interrupted.
import * as net from "node:net"
import { Data, Effect } from "effect"
class ListenError extends Data.TaggedError("ListenError")<{
readonly cause: unknown
}> {}
// Variant 1: `resume` is never referenced on any code path. The server starts
// listening, but nothing ever completes the effect.
export const listenForever = (port: number): Effect.Effect<net.AddressInfo, ListenError> =>
Effect.callback<net.AddressInfo, ListenError>((_resume) => {
const server = net.createServer()
server.listen(port)
// forgot to wire 'listening'/'error' events to resume
})
// Variant 2: `resume` is invoked only in the error branch. If the server binds
// successfully, the 'listening' event fires but the fiber never resumes.
export const listenErrorsOnly = (port: number): Effect.Effect<net.AddressInfo, ListenError> =>
Effect.callback<net.AddressInfo, ListenError>((resume) => {
const server = net.createServer()
server.once("error", (cause) => {
resume(Effect.fail(new ListenError({ cause })))
})
server.listen(port)
// success branch missing: 'listening' never calls resume(Effect.succeed(...))
})
const program = Effect.flatMap(listenForever(0), (address) => Effect.log(address.port))
void program
Good
// RULE: effect-callback-never-resumes
// GOOD: every terminal outcome of the async operation is wired to `resume`:
// the 'error' event fails the effect and the 'listening' event succeeds it,
// so the fiber always completes. The returned cleanup effect tears the server
// down if the fiber is interrupted while still suspended.
import * as net from "node:net"
import { Data, Effect } from "effect"
class ListenError extends Data.TaggedError("ListenError")<{
readonly cause: unknown
}> {}
export const listen = (port: number): Effect.Effect<net.AddressInfo, ListenError> =>
Effect.callback<net.AddressInfo, ListenError>((resume) => {
const server = net.createServer()
server.once("error", (cause) => {
resume(Effect.fail(new ListenError({ cause })))
})
server.once("listening", () => {
const address = server.address()
if (address === null || typeof address === "string") {
server.close()
resume(Effect.fail(new ListenError({ cause: "no address info" })))
} else {
resume(Effect.succeed(address))
}
})
server.listen(port)
// interruption cleanup: close the server if nobody resumed yet
return Effect.sync(() => {
server.close()
})
})
const program = Effect.flatMap(listen(0), (address) => Effect.log(address.port))
void program
Where this came up
Mined from the Effect Office Hours playlist; deduplicated against all implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
effectCallbackNeverResumes
Problem
Inside Effect.callback((resume) => { ... }), flags bodies where the resume parameter is never invoked (or never referenced) on any code path, or is only invoked in an error branch while the success branch never resumes. The types are satisfied regardless of whether resume is called, but the resulting effect can only complete via resume.
Why the compiler is silent / what breaks at runtime: The effect type-checks with any success/error type you claim, but a callback that never calls resume produces a fiber that suspends forever — the program silently hangs until externally interrupted.
Both examples below type-check with zero errors against
effect@4.0.0-beta.104(re-verified with an isolated strict tsconfig) — so the compiler offers no protection here and a diagnostic is the only static safety net.Bad — compiles cleanly, the rule should flag this
Good
Where this came up
Mined from the Effect Office Hours playlist; deduplicated against all implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
effectCallbackNeverResumes