Problem
In Effect.mapError / Effect.catchAll / Effect.try / Effect.tryPromise handlers that construct a new tagged error, flags handlers whose caught-error parameter is never referenced in the constructor arguments. Wrapping an implementation-level failure (e.g. a Postgres driver error behind an abstract service error) without passing it as the cause property throws away the only diagnostic information about what actually failed.
Why the compiler is silent / what breaks at runtime: Type-checks identically, but at runtime the rendered failure and logs show only the abstract wrapper error; the concrete underlying error (SQL message, driver code) is silently lost, making production incidents undiagnosable.
Both examples below type-check with zero errors against effect@4.0.0-beta.101 (latest main of Effect-TS/effect) under the monorepo's strict tsconfig, verified with an isolated per-proposal 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: wrapped-error-drops-cause
// BAD: These handlers wrap a lower-level failure in an abstract tagged error
// but never reference the caught error in the constructor arguments, so the
// concrete failure (SQL message, driver code, thrown exception) is thrown
// away. The wrapper type-checks identically, but at runtime the rendered
// failure shows only "database operation failed" — production incidents
// become undiagnosable. The linter flags handlers whose caught-error
// parameter never flows into the constructed error (e.g. as `cause`).
import { Data, Effect } from "effect"
class NoteError extends Data.TaggedError("NoteError")<{
message: string
cause?: unknown
}> {}
declare const runQuery: (sql: string) => Effect.Effect<ReadonlyArray<string>, Error>
declare const driverQuery: (sql: string) => Promise<ReadonlyArray<string>>
// flagged: mapError handler ignores the underlying driver error entirely
export const listNotes = runQuery("SELECT * FROM notes").pipe(
Effect.mapError(() => new NoteError({ message: "database operation failed" }))
)
// flagged: the parameter is declared but never used in the new error
export const listNotesCatch = runQuery("SELECT * FROM notes").pipe(
Effect.catch((_error) => Effect.fail(new NoteError({ message: "database operation failed" })))
)
// flagged: Effect.tryPromise catch drops the thrown value on the floor
export const listNotesTry = Effect.tryPromise({
try: () => driverQuery("SELECT * FROM notes"),
catch: () => new NoteError({ message: "database operation failed" })
})
void listNotes
void listNotesCatch
void listNotesTry
Good
// RULE: wrapped-error-drops-cause
// GOOD: When wrapping an implementation-level failure in an abstract tagged
// error, attach the caught error as the `cause` property. The abstract error
// still hides the driver from the public API, but the concrete failure is
// preserved and rendered as part of the failure/stack trace, keeping
// production incidents diagnosable.
import { Data, Effect } from "effect"
class NoteError extends Data.TaggedError("NoteError")<{
message: string
cause?: unknown
}> {}
declare const runQuery: (sql: string) => Effect.Effect<ReadonlyArray<string>, Error>
declare const driverQuery: (sql: string) => Promise<ReadonlyArray<string>>
// ok: the underlying error travels along as the cause
export const listNotes = runQuery("SELECT * FROM notes").pipe(
Effect.mapError((cause) => new NoteError({ message: "database operation failed", cause }))
)
// ok: catch handler forwards the caught error as cause
export const listNotesCatch = runQuery("SELECT * FROM notes").pipe(
Effect.catch((cause) => Effect.fail(new NoteError({ message: "database operation failed", cause })))
)
// ok: the thrown value from the promise is captured as cause
export const listNotesTry = Effect.tryPromise({
try: () => driverQuery("SELECT * FROM notes"),
catch: (cause) => new NoteError({ message: "database operation failed", cause })
})
void listNotes
void listNotesCatch
void listNotesTry
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
wrappedErrorDropsCause
Problem
In Effect.mapError / Effect.catchAll / Effect.try / Effect.tryPromise handlers that construct a new tagged error, flags handlers whose caught-error parameter is never referenced in the constructor arguments. Wrapping an implementation-level failure (e.g. a Postgres driver error behind an abstract service error) without passing it as the cause property throws away the only diagnostic information about what actually failed.
Why the compiler is silent / what breaks at runtime: Type-checks identically, but at runtime the rendered failure and logs show only the abstract wrapper error; the concrete underlying error (SQL message, driver code) is silently lost, making production incidents undiagnosable.
Both examples below type-check with zero errors against
effect@4.0.0-beta.101(latestmainof Effect-TS/effect) under the monorepo's strict tsconfig, verified with an isolated per-proposal 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
wrappedErrorDropsCause