{"record":{"id":"e64494a59ade760a","repo":"SeaQL/sea-orm","slug":"fail-to-rollback-transaction","errorCode":null,"errorMessage":"Fail to rollback transaction","messagePattern":"Fail to rollback transaction","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"sea-orm-sync/src/database/transaction.rs","lineNumber":375,"sourceCode":"            }\n        }\n        Ok(())\n    }\n}\n\nimpl TransactionSession for DatabaseTransaction {\n    fn commit(self) -> Result<(), DbErr> {\n        self.commit()\n    }\n\n    fn rollback(self) -> Result<(), DbErr> {\n        self.rollback()\n    }\n}\n\nimpl Drop for DatabaseTransaction {\n    fn drop(&mut self) {\n        self.start_rollback().expect(\"Fail to rollback transaction\");\n    }\n}\n\nimpl ConnectionTrait for DatabaseTransaction {\n    fn get_database_backend(&self) -> DbBackend {\n        // this way we don't need to lock just to know the backend\n        self.backend\n    }\n\n    #[instrument(level = \"trace\", skip(stmt))]\n    #[allow(unused_variables)]\n    fn execute_raw(&self, stmt: Statement) -> Result<ExecResult, DbErr> {\n        debug_print!(\"{}\", stmt);\n\n        super::tracing_spans::with_db_span!(\n            \"sea_orm.execute\",\n            self.backend,\n            stmt.sql.as_str(),","sourceCodeStart":357,"sourceCodeEnd":393,"githubUrl":"https://github.com/SeaQL/sea-orm/blob/e29bcd1b417c41a553b386fe94511d7c64a1c8ec/sea-orm-sync/src/database/transaction.rs#L357-L393","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Explicitly call `.commit()` or `.rollback()` on every transaction instead of relying on `Drop`; never let a transaction value be discarded.","Ensure no other clone/lock of the transaction's connection exists when it goes out of scope — don't share `DatabaseTransaction` across tasks/threads.","Check that the connection is still alive; re-establish and retry the transaction if the backend disconnected.","Wrap the transaction body so failures flow through `.rollback()` (which returns `Result`) rather than the panicking `Drop` path.","If the panic is spurious in your runtime, pin to a sea-orm version where Drop rollback is non-panicking or report upstream."],"exampleFix":"// before — relies on Drop, panics if the connection lock is held\n{\n    let txn = db.begin()?;\n    do_work(&txn)?;\n    // early return / drop with txn still open & locked -> panic\n}\n// after — explicit rollback handles errors as values\n{\n    let txn = db.begin()?;\n    match do_work(&txn) {\n        Ok(v) => txn.commit().map(|_| v),\n        Err(e) => { let _ = txn.rollback(); Err(e) }\n    }\n}","handlingStrategy":"try-catch","validationCode":"// Before relying on Drop-rollback, verify the transaction is closeable\nfn can_finish_transaction(txn: &DatabaseTransaction) -> bool {\n    txn.get_database_backend() != DbBackend::Mock // mock backend has no real rollback\n        && !std::thread::panicking()\n}","typeGuard":"fn transaction_is_open_and_unlocked(txn: &DatabaseTransaction) -> bool {\n    // `open` is private; approximate by tracking commit/rollback yourself\n    TRACKER.with(|t| t.borrow().contains_key(&(txn as *const _ as usize)))\n}","tryCatchPattern":"// Drop cannot be caught where it happens, but you can isolate the panic:\nlet result = std::panic::catch_unwind(AssertUnwindSafe(|| {\n    let txn = db.begin()?;\n    run_in_txn(&txn)?;\n    txn.commit()\n}));\nif result.is_err() {\n    eprintln!(\"transaction dropped uncleanly (rollback panic or user panic)\");\n}","preventionTips":["Always call .commit() or .rollback() explicitly; never drop an open transaction","Never clone or move a DatabaseTransaction into another thread/task that outlives your scope","Handle the body error first, then rollback, so Drop never runs on a locked transaction","Keep transactions short and scoped; avoid holding the connection lock across await/long work","Monitor connection health (ping) before long transactions to avoid disconnected-drop panics"],"tags":["panic","transaction","rollback","drop","sqlite"],"backgroundTag":"internal-invariant-violation","analyzedSha":"e29bcd1b417c41a553b386fe94511d7c64a1c8ec","analyzedAt":"2026-09-10T11:31:52.468Z","contentChangedAt":"2026-09-10T11:31:52.468Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}