SQL Server error 3930
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
An earlier error doomed your transaction: it can only be rolled back. Error 3930 means you tried to write or commit in it anyway; 3998 means a batch ended with it still open, so SQL Server rolled it back. In CATCH, check XACT_STATE() and roll back before doing anything else.
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
Tested on SQL Server 2022 (16.0.4295.3); also 2019 (15.0.4490.9) and 2025 (17.0.5005.3) · Updated 9 October 2026
What it means
Some errors leave a transaction doomed (Microsoft’s word is uncommittable): it’s still open and still holds its locks, but the only thing it can do is roll back. Two errors follow from that:
- 3930 “The current transaction cannot be committed and cannot support operations that write to
the log file. Roll back the transaction.” You ran a
COMMIT, or a statement that writes (anINSERTinto a log table, say), inside a doomed transaction. - 3998 “Uncommittable transaction is detected at the end of the batch. The transaction is rolled back.” A batch finished with a doomed transaction still open, so SQL Server rolled it back for you.
Neither is the real problem. The real problem is the error before them, the one that doomed the
transaction, and your CATCH block (or the code after it) has hidden it.
XACT_STATE() tells you where you are:
XACT_STATE() | Meaning | What you can do |
|---|---|---|
| 1 | A transaction is open and can commit | Commit, roll back, or keep going |
| 0 | No transaction | Nothing to roll back |
| -1 | A transaction is open but doomed | Roll back only |
Common causes
SET XACT_ABORT ONplus any error insideTRY. WithXACT_ABORTon, every run-time error dooms the transaction, so theCATCHblock always starts atXACT_STATE() = -1.- An error that aborts the batch, inside
TRY, even withXACT_ABORToff: a conversion error such asConversion failed, a deadlock (1205), and others that would normally end the batch. InsideTRYthey jump toCATCHwith the transaction doomed. - A
CATCHblock that logs first and rolls back second. It writes the error to a table while the transaction is doomed: 3930. - A
CATCHblock that swallows the error without rolling back, so the batch ends with the doomed transaction open: 3998. - Triggers, where
XACT_ABORTis on by default, so an error inside one behaves as in cause 1.
How to fix it
Roll back first, then do the rest
In CATCH, roll back before anything that writes, keep the error details in variables, and log
after the rollback, outside the transaction:
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
INSERT INTO orders (id, account_id, total, status) VALUES (1, 1, 10.00, 'open');
COMMIT;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK; -- first, whatever the state
INSERT INTO error_log (number, message) -- now outside any transaction
VALUES (ERROR_NUMBER(), ERROR_MESSAGE());
THROW; -- re-raise the original error
END CATCH;
ERROR_NUMBER() and ERROR_MESSAGE() still work after the rollback, anywhere in the CATCH
block. THROW without arguments re-raises the original error, so the caller sees the duplicate key
(or whatever it was) instead of a vague 3930.
Use XACT_ABORT ON, and expect -1
Turning XACT_ABORT on is the usual advice: any run-time error rolls back the whole transaction, so
you can’t commit half a unit of work by accident. The price is that the CATCH block always sees a
doomed transaction, which is fine if it rolls back first. (Compile errors, such as a misspelt column,
aren’t affected by XACT_ABORT: the batch never starts.)
Only commit when XACT_STATE() is 1
If you want to keep a transaction going after an error you expected (with XACT_ABORT off and a
statement-level error such as a duplicate key), check first:
BEGIN CATCH
IF XACT_STATE() = 1
COMMIT; -- or carry on: the transaction is still good
ELSE IF XACT_STATE() = -1
ROLLBACK;
END CATCH;
Don’t use @@TRANCOUNT for this: it’s 1 for a doomed transaction too.
Log what doomed it
When you only see 3930 or 3998 in an application log, the original error went into a CATCH block
that didn’t re-raise it. Add THROW; (or log ERROR_NUMBER(), ERROR_MESSAGE() and
ERROR_LINE()) in that block to find the real error.
Reproduce it
On SQL Server 2022 (RTM-CU27, 16.0.4295.3), with seo_sqlerr_conn.orders already holding a row with
id 1, and an error_log table:
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRAN;
UPDATE seo_sqlerr_conn.accounts SET balance = balance - 10 WHERE id = 1;
INSERT INTO seo_sqlerr_conn.orders (id, account_id, total, status) VALUES (1, 1, 10.00, 'open');
COMMIT;
END TRY
BEGIN CATCH
PRINT CONCAT('caught ', ERROR_NUMBER(), ', XACT_STATE() = ', XACT_STATE());
INSERT INTO seo_sqlerr_conn.error_log (number, message) VALUES (ERROR_NUMBER(), ERROR_MESSAGE());
ROLLBACK;
END CATCH;
caught 2627, XACT_STATE() = -1
Msg 3930, Level 16, State 1, Server 7732422b7f56, Line 11
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
The duplicate key (2627) doomed the transaction; the
INSERT into the log table got 3930; with XACT_ABORT on, that error ended the batch and rolled the
transaction back (@@TRANCOUNT was 0 afterwards), so the ROLLBACK line never ran. A COMMIT in
the same place got the same 3930.
With XACT_ABORT off, a conversion error and a CATCH block that only prints:
BEGIN TRY
BEGIN TRAN;
UPDATE seo_sqlerr_conn.accounts SET balance = balance - 10 WHERE id = 1;
SELECT CAST('abc' AS int);
COMMIT;
END TRY
BEGIN CATCH
PRINT CONCAT('caught ', ERROR_NUMBER(), ', XACT_STATE() = ', XACT_STATE());
END CATCH;
caught 245, XACT_STATE() = -1
Msg 3998, Level 16, State 1, Server 7732422b7f56, Line 1
Uncommittable transaction is detected at the end of the batch. The transaction is rolled back.
The same duplicate key with XACT_ABORT off left the transaction committable
(XACT_STATE() = 1), and the log-table INSERT worked, but the ROLLBACK after it undid that row
too: one more reason to roll back first and log afterwards. SQL Server 2019 (15.0.4490.9) and 2025
(17.0.5005.3) gave the same results.
The fixed CATCH block from above (roll back, log, THROW) on the same duplicate key returned the
original error instead of 3930, kept the log row, and left the balance unchanged:
Msg 2627, Level 14, State 1, Server 7732422b7f56, Line 6
Violation of PRIMARY KEY constraint 'PK__orders__3213E83F9CA27086'. Cannot insert duplicate key in object 'seo_sqlerr_conn.orders'. The duplicate key value is (1).
In Inlet
Query tabs run T-SQL a batch at a time (GO splits batches) and show PRINT messages with the
results, so you can step through a TRY…CATCH block like the ones above and see XACT_STATE() at
each point. Edits you make in the grid are staged until you commit (⌘S), and Review shows the
statements that will run first. When a statement fails with 3930 or 3998, the error links to this
page.
Related
- Transaction (Process ID …) was deadlocked on lock resources with another process and has been chosen as the deadlock victim
- Lock request time out period exceeded. (SQL Server error 1222)
- Violation of PRIMARY KEY constraint. Cannot insert duplicate key in object
- Conversion failed when converting the varchar value to data type int
- current transaction is aborted, commands ignored until end of transaction block