Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

sql: `withTransaction` turns a failed COMMIT into a defect, not a SqlError

Closed
#8,860 0 comments 1 reaction 0 assignees View on GitHub

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 orDie came with the sqlfx import (#2104) with no stated reason.
  • #7236 made a failed BEGIN a typed SqlError. It left COMMIT unchanged.
  • #8257 made @effect/sql-sqlite-do fail a rejected native commit with a typed SqlError.
  • #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 COMMIT that PostgreSQL answers with ROLLBACK. It turns that failure into a defect "the same way it handles any failed COMMIT", 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Effect-TS/effect

All issues in Effect-TS/effect

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.