- Error number
- 3930
- Severity
- 16 (user-correctable error)
- State
- 1
- Product
- Microsoft SQL Server Database Engine
- Versions
- All supported SQL Server versions (2016 to 2025), Azure SQL Database, Azure SQL Managed Instance
- Category
- Transactions
- Last reviewed
- 23 October 2026
On this page
What it means
Error 3930 means an earlier error left the transaction uncommittable (doomed), and the code then tried to commit it or write more data inside it; the only thing it can still do is roll back.
Inside a TRY block, some errors do not just fail the statement, they doom the whole transaction: every error when SET XACT_ABORT ON is in effect, and with it OFF, errors that would otherwise end the batch, such as a conversion failure. A doomed transaction still holds its locks, but it can only read and roll back. Any INSERT, UPDATE, DELETE, or a COMMIT, raises 3930 at severity 16.
The function that tells you where you stand is XACT_STATE(): 1 means there is an active transaction that can be committed, -1 means there is a transaction but it is doomed, and 0 means there is no transaction. @@TRANCOUNT cannot tell 1 and -1 apart, which is why CATCH blocks that test only @@TRANCOUNT and then commit or log run into this error.
Common causes
- A CATCH block writes to a log table before rolling back, while the transaction is already doomed.
- A CATCH block decides to commit because @@TRANCOUNT is greater than 0, without checking XACT_STATE().
SET XACT_ABORT ONin the session or procedure, which makes every error in a TRY block doom the transaction, including duplicate keys and constraint violations.- An error such as a failed conversion (error 245) inside the TRY block, which dooms the transaction even with XACT_ABORT OFF.
Diagnose it
Read-only These queries only read metadata and dynamic management views. They do not change data, permissions or configuration, so they are safe to run first.
Ask SQL Server whether the transaction can still be committed
Run this in the CATCH block (or right after the failure in the same session).
SELECT XACT_STATE() AS xact_state, -- 1 committable, -1 doomed, 0 none
@@TRANCOUNT AS trancount; xact_state = -1 confirms a doomed transaction: roll back before doing anything else.
Is XACT_ABORT ON for this session?
SELECT CASE WHEN (@@OPTIONS & 16384) = 16384 THEN 'ON' ELSE 'OFF' END AS xact_abort; ON means any error inside the TRY block dooms the transaction, so the CATCH block must always roll back.
Read the original error in the CATCH block
3930 is a symptom. The error that doomed the transaction is the one to fix; capture it before anything else in the CATCH block.
SELECT ERROR_NUMBER() AS error_number, ERROR_MESSAGE() AS error_message,
ERROR_PROCEDURE() AS error_procedure, ERROR_LINE() AS error_line; Inside a CATCH block: the first error, for example 2627 or 245. Outside a CATCH block all four columns are NULL.
How to fix it
Each fix is labelled with what it changes. Try read-only checks and the least invasive fix first, and test changes outside production.
Roll back first in every CATCH block, then log and rethrow
Changes dataTest XACT_STATE(), roll back if there is any transaction, and only then write to the log table: after the rollback the insert runs outside the doomed transaction. THROW passes the original error to the caller.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO dbo.Orders (id, qty) VALUES (1, 7);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.ErrorLog (message) VALUES (ERROR_MESSAGE());
THROW;
END CATCH;
Commit only when XACT_STATE() is 1
Changes dataIf your CATCH block is meant to keep the work done so far (only possible with XACT_ABORT OFF and an error that did not doom the transaction), commit when the state is 1 and roll back when it is -1.
BEGIN CATCH
IF XACT_STATE() = 1 COMMIT TRANSACTION; -- still committable
IF XACT_STATE() = -1 ROLLBACK TRANSACTION; -- doomed: roll back
END CATCH;
Keep error details in a table variable across the rollback
Changes dataRows in a table variable are not removed by ROLLBACK. Collect details there while the transaction is doomed, roll back, then save them to the permanent log.
BEGIN CATCH
DECLARE @err TABLE (number INT, message NVARCHAR(4000));
INSERT INTO @err VALUES (ERROR_NUMBER(), ERROR_MESSAGE());
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
INSERT INTO dbo.ErrorLog (message) SELECT message FROM @err;
THROW;
END CATCH;
Examples
Logging the error inside a doomed transaction (XACT_ABORT ON)
SET NOCOUNT ON;
CREATE TABLE dbo.Orders (id INT PRIMARY KEY, qty INT);
CREATE TABLE dbo.ErrorLog (logged_at DATETIME2 DEFAULT SYSDATETIME(), message NVARCHAR(4000));
GO
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO dbo.Orders VALUES (1, 5);
INSERT INTO dbo.Orders VALUES (1, 7); -- duplicate key dooms the transaction
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- XACT_STATE() is -1 here: the transaction is doomed
INSERT INTO dbo.ErrorLog (message) VALUES (ERROR_MESSAGE()); -- a write: not allowed
END CATCH;
Msg 3930, Level 16, State 1, Line 10 The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
Reproduced on SQL Server 2025 (17.0.1145.4), 23 October 2026.
Committing after a conversion error (XACT_ABORT OFF)
SET NOCOUNT ON;
CREATE TABLE dbo.Orders (id INT PRIMARY KEY, qty INT);
GO
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO dbo.Orders VALUES (1, 5);
INSERT INTO dbo.Orders VALUES (2, CAST('five' AS INT)); -- error 245 dooms the transaction
END TRY
BEGIN CATCH
-- here XACT_STATE() = -1 but @@TRANCOUNT = 1
IF @@TRANCOUNT > 0 COMMIT TRANSACTION; -- @@TRANCOUNT cannot see the transaction is doomed
END CATCH;
Msg 3930, Level 16, State 1, Line 8 The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction. Msg 3998, Level 16, State 1, Line 1 Uncommittable transaction is detected at the end of the batch. The transaction is rolled back.
Reproduced on SQL Server 2025 (17.0.1145.4), 23 October 2026.
How to prevent it
Use one CATCH block pattern everywhere: IF XACT_STATE() <> 0 ROLLBACK TRANSACTION; as the first statement, then logging, then THROW. Many teams also put SET XACT_ABORT ON at the top of procedures that open transactions, so a client timeout or any error never leaves a transaction open; with that setting, rolling back in the CATCH block is mandatory.
Never decide to commit from @@TRANCOUNT alone. For a refresher on how transactions and rollbacks behave, see SQL transactions and ACID explained.
Common questions
What is a doomed transaction in SQL Server?
A transaction that an error inside a TRY block has made uncommittable. It still exists and holds its locks, XACT_STATE() returns -1, and the only allowed actions are reads and ROLLBACK.
What is the difference between XACT_STATE() and @@TRANCOUNT?
@@TRANCOUNT counts open BEGIN TRANSACTION levels. XACT_STATE() returns 1, 0 or -1 and is the only one that tells you whether the open transaction can still be committed.
Why do I also get error 3998 after 3930?
When the batch ends with a doomed transaction still open, SQL Server rolls it back for you and reports 3998. Rolling back in your CATCH block avoids both messages.
Learn more on CodeWithSQL
- SQL transactions and ACID explained
- Managing transactions and concurrency
- Transactions, locks and deadlocks
Official documentation
- XACT_STATE (Transact-SQL)
- TRY...CATCH (Transact-SQL)
- SET XACT_ABORT (Transact-SQL)
- Database Engine events and errors 3000 to 3999
Last technically reviewed 23 October 2026; examples and diagnostics run on SQL Server 2025 (17.0.1145.4). Message text checked against the engine's sys.messages catalogue.