SeaQL/sea-orm · critical

Fail to rollback transaction

Error message

Fail to rollback transaction

What it means

`DatabaseTransaction`'s `Drop` impl calls `start_rollback()` and panics with "Fail to rollback transaction" if rollback cannot be arranged. `start_rollback` fails when the transaction is still open but its inner connection mutex cannot be locked ("Dropping a locked Transaction") or the connection reports disconnected. Because `Drop` cannot return an error, the library chooses to panic — usually signaling the transaction was dropped while its connection lock is still held elsewhere, or the underlying connection died.

Source

Thrown at sea-orm-sync/src/database/transaction.rs:375

            }
        }
        Ok(())
    }
}

impl TransactionSession for DatabaseTransaction {
    fn commit(self) -> Result<(), DbErr> {
        self.commit()
    }

    fn rollback(self) -> Result<(), DbErr> {
        self.rollback()
    }
}

impl Drop for DatabaseTransaction {
    fn drop(&mut self) {
        self.start_rollback().expect("Fail to rollback transaction");
    }
}

impl ConnectionTrait for DatabaseTransaction {
    fn get_database_backend(&self) -> DbBackend {
        // this way we don't need to lock just to know the backend
        self.backend
    }

    #[instrument(level = "trace", skip(stmt))]
    #[allow(unused_variables)]
    fn execute_raw(&self, stmt: Statement) -> Result<ExecResult, DbErr> {
        debug_print!("{}", stmt);

        super::tracing_spans::with_db_span!(
            "sea_orm.execute",
            self.backend,
            stmt.sql.as_str(),

View on GitHub (pinned to e29bcd1b41)

Solutions

  1. Explicitly call `.commit()` or `.rollback()` on every transaction instead of relying on `Drop`; never let a transaction value be discarded.
  2. Ensure no other clone/lock of the transaction's connection exists when it goes out of scope — don't share `DatabaseTransaction` across tasks/threads.
  3. Check that the connection is still alive; re-establish and retry the transaction if the backend disconnected.
  4. Wrap the transaction body so failures flow through `.rollback()` (which returns `Result`) rather than the panicking `Drop` path.
  5. If the panic is spurious in your runtime, pin to a sea-orm version where Drop rollback is non-panicking or report upstream.

Example fix

// before — relies on Drop, panics if the connection lock is held
{
    let txn = db.begin()?;
    do_work(&txn)?;
    // early return / drop with txn still open & locked -> panic
}
// after — explicit rollback handles errors as values
{
    let txn = db.begin()?;
    match do_work(&txn) {
        Ok(v) => txn.commit().map(|_| v),
        Err(e) => { let _ = txn.rollback(); Err(e) }
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before relying on Drop-rollback, verify the transaction is closeable
fn can_finish_transaction(txn: &DatabaseTransaction) -> bool {
    txn.get_database_backend() != DbBackend::Mock // mock backend has no real rollback
        && !std::thread::panicking()
}

Type guard

fn transaction_is_open_and_unlocked(txn: &DatabaseTransaction) -> bool {
    // `open` is private; approximate by tracking commit/rollback yourself
    TRACKER.with(|t| t.borrow().contains_key(&(txn as *const _ as usize)))
}

Try / catch

// Drop cannot be caught where it happens, but you can isolate the panic:
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
    let txn = db.begin()?;
    run_in_txn(&txn)?;
    txn.commit()
}));
if result.is_err() {
    eprintln!("transaction dropped uncleanly (rollback panic or user panic)");
}

Prevention

When it happens

Trigger: Dropping a `DatabaseTransaction` that was never committed or rolled back, while: (1) the internal connection `Mutex` is still locked by another clone/task (e.g. the transaction was cloned or moved into a task that holds the lock), (2) the connection is in a disconnected/invalid backend state, or (3) in sync (blocking) code, the transaction lives across a thread where the lock is contended at drop time.

Common situations: Commonly hit when a transaction is accidentally dropped mid-use (e.g. `?` early-return while holding the connection lock via `begin()`/raw access), when spawning tasks that share the transaction, or when the database connection was killed (network loss, server restart) before drop. Users see a panic during scope exit, often masking the original error.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of SeaQL/sea-orm@e29bcd1b41 (2026-09-10). Data as JSON: /api/errors/e64494a59ade760a. Report an issue: GitHub.