sql: `withTransaction` turns a failed COMMIT into a defect, not a SqlError
Maintainers usually reply within 1 day
A pull request for this has already been merged.
- #8892 by @effect-bot — merged
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 67/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- postgresql, typescript
- Domain
- databases
Research direction
Start at packages/effect/src/sql/SqlClient.ts:432 (Effect.orDie(options.commit(conn))) and read the surrounding withTransaction plus the onCommitFailure logic added in #8529. The linked history matters: #8558's tests in packages/pg assert the defect for a ROLLBACK-answered COMMIT and will need updating, and #7236 shows the precedent for turning BEGIN failures into typed SqlError. Done means the reproduction in the issue fails (not dies) with UniqueViolation so catchTag("SqlError") catches it, ROLLBACK/RELEASE SAVEPOINT failures stay defects, and the maintainer answers the two open questions (cleanup-failure cause, patch vs v4/next-minor) before merging.
Written by the indexing model from the issue text.
Description
Summary
When PostgreSQL rejects COMMIT, withTransaction dies instead of failing with a SqlError. PostgreSQL reports deferred constraint violations and serialization failures (40001) at COMMIT. The driver classifies them (UniqueViolation, SerializationError), but catchTag("SqlError") and typed retry policies never see them. withTransaction already returns Effect<A, E | SqlError, R> (packages/effect/src/sql/SqlClient.ts:63).
Is the defect intended? If not, I have a PR ready (2 source lines).
Versions: [email protected] and @effect/[email protected]. Same code on main at 2131d44bd and on the head of #8735. PostgreSQL 18, Node 26.8.2.
Reproduction
import { Cause, Effect, Exit, Redacted } from "effect"
import { PgClient } from "@effect/sql-pg"
import * as Reactivity from "effect/reactivity/Reactivity"
const url = Redacted.make("postgres://postgres:[email protected]:5432/postgres")
const show = (label, exit) =>
console.log(label, Exit.isSuccess(exit) ? exit.value : {
hasFails: Cause.hasFails(exit.cause),
hasDies: Cause.hasDies(exit.cause),
reason: Cause.squash(exit.cause).reason._tag
})
const program = Effect.gen(function*() {
const a = yield* PgClient.make({ url, maxConnections: 1 })
yield* a`drop table if exists dfr`
yield* a`create table dfr (id int unique deferrable initially deferred)`
show("deferred unique", yield* Effect.exit(a.withTransaction(a`insert into dfr values (1), (1)`)))
show("catchTag", yield* Effect.exit(a.withTransaction(a`insert into dfr values (2), (2)`).pipe(
Effect.catchTag("SqlError", (e) => Effect.succeed(`caught ${e.reason._tag}`))
)))
})
await Effect.runPromise(Effect.scoped(program).pipe(Effect.provide(Reactivity.layer)))
Actual:
deferred unique { hasFails: false, hasDies: true, reason: 'UniqueViolation' }
catchTag { hasFails: false, hasDies: true, reason: 'UniqueViolation' }
A write skew under SERIALIZABLE dies the same way with SerializationError. The server log shows each error on STATEMENT: COMMIT.
Expected: each exit fails with the SqlError, and catchTag returns caught UniqueViolation.
Cause
packages/effect/src/sql/SqlClient.ts:432: effect = Effect.orDie(options.commit(conn))
History
- The
orDiecame with the sqlfx import (#2104) with no stated reason. - #7236 made a failed
BEGINa typedSqlError. It leftCOMMITunchanged. - #8257 made
@effect/sql-sqlite-dofail a rejected native commit with a typedSqlError. - #8529 (merged 2026-09-25) added
onCommitFailure. Its body says: "Preserve the original COMMIT defect and include cleanup failure in its cause." - #8558 (2026-09-27) fails a pg
COMMITthat PostgreSQL answers withROLLBACK. It turns that failure into a defect "the same way it handles any failedCOMMIT", and its tests assert the defect.
Proposal
Drop the orDie, so withTransaction fails with the SqlError from commit. The public type does not change. ROLLBACK and RELEASE SAVEPOINT failures stay defects.
One gap: when SQLite onCommitFailure cleanup fails, the cause becomes Fail(SqlError) plus Die(cleanup). catchTag("SqlError") and Effect.catch recover and drop the Die, so the caller never sees the cleanup failure. The connection stays poisoned, so the next query still fails; no data is lost. Should a cleanup failure keep the whole exit a defect?
This changes behavior: Effect.catch now also sees COMMIT failures. Should it go to main as a patch, like #7236, or to v4/next-minor?
- Dominant language
- TypeScript
- Stars
- 16.7k
- Forks
- 808
- Avg merge
- 11h 28m
- Merged PRs (30d)
- 490
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Effect-TS/effect
-
Difficulty 1/5 1-3 hours Newbie friendliness 86/100
Effect-TS/effect#8863 · 1 comment ·
Maintainers usually reply within 1 day
-
BrowserWorkerRunner: port finalizer throws when the worker global has no close() (Bun)Possibly taken @santiago-ramos-02 claimed this 9 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Effect-TS/effect#8635 · 3 comments ·
Maintainers usually reply within 1 day
-
Support {self: this} for fnUntracedMay be free again @ArjunCodess claimed this 20 days ago, and no pull request is open. Openenhancement
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Effect-TS/effect#8101 · 1 comment ·
Maintainers usually reply within 1 day
-
add Effect-native McpClientPossibly taken @lloydrichards claimed this 3 days ago. Openenhancement
Difficulty 5/5 Over a week Newbie friendliness 8/100
Effect-TS/effect#8912 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 48/100
Maintainers usually reply within 1 day
All issues in Effect-TS/effect
Similar issues
-
[Docs] README: FAQ setup command, IDA in the intro, Node badgePossibly taken @akram1089 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
morluto/rea#1353 · 1 comment ·
Maintainers usually reply within 1 day
-
[Feature]: [P3] engine-rs: the package source hash should ignore line endings and untracked filesOpen
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
maniator/verticopolis#880 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
siyuan-note/siyuan#20353 ·
Maintainers usually reply within 1 day
-
afk-ok area:data-quality importer size:S
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
enorm-labs/event-junkie#3027 ·
Maintainers usually reply within 1 day