The Current Transaction Cannot Be Committed (Error 3930)

🚨Part of the SQL Server Errors series, the exact messages and what actually causes them.In: Performance & Troubleshooting

Msg 3930  ·  Level 16  ·  State 1
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
⚡Error 3930 is never the real error. Something earlier failed and doomed the transaction (XACT_STATE() returns -1), then the code tried to write or commit anyway, usually an error-logging INSERT in a CATCH block. Find the first error, then fix the CATCH: save ERROR_NUMBER() and ERROR_MESSAGE(), ROLLBACK when XACT_STATE() <> 0, log after the rollback, and THROW.

A procedure or a trigger fails with Msg 3930 and the application log shows nothing else. The message tells you to roll back, but it does not tell you what went wrong, because the error that caused it was caught and swallowed a few lines earlier.


What Error 3930 Means

An error inside a transaction can leave it in one of two states. Some errors only fail the statement and the transaction carries on. Others doom it: the transaction stays open, holds its locks, and can do nothing except roll back. Any statement that writes to the log after that point, including COMMIT, gets Msg 3930. XACT_STATE() tells you which state you are in:

XACT_STATE()What it means
1Open and committable. The failed statement was undone, the rest of the transaction is intact, and you can still write, commit or roll back.
0No transaction open. Nothing to commit or roll back.
-1Open and doomed. Reads still work; any write or COMMIT raises 3930. The only way out is ROLLBACK.

@@TRANCOUNT cannot tell 1 from -1; it returns the same nonzero count for both (1 in this repro, more inside nested transactions), which is why code that checks IF @@TRANCOUNT > 0 walks straight into 3930. This is the smallest repro: XACT_ABORT on, a foreign key violation, and a CATCH block that logs the error to a table.

SET XACT_ABORT ON;
BEGIN TRY
    BEGIN TRANSACTION;
    INSERT dbo.Orders (CustomerId, Amount) VALUES (99, 10); -- customer 99 does not exist
    COMMIT;
END TRY
BEGIN CATCH
    SELECT XACT_STATE() AS xact_state, @@TRANCOUNT AS trancount, ERROR_NUMBER() AS error_number;
    INSERT dbo.ErrorLog (ErrNo, ErrMsg) VALUES (ERROR_NUMBER(), ERROR_MESSAGE());
END CATCH;
PRINT 'after catch';
The CATCH block, then the logging INSERTreal example output, not something to copy
xact_state trancount error_number ---------- ----------- ------------ -1 1 547 Msg 3930, Level 16, State 1, Server HPAI01, Line 10 The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.

Three things in that output. The real error was 547, and the client never saw it. The log row was never written, so the error table is empty too. And after catch never printed, because with XACT_ABORT on the 3930 ended the batch, which rolled the transaction back.


Which Errors Doom a Transaction

The same foreign key violation does not always doom the transaction. It depends on XACT_ABORT and on the error. Captured on the lab:

The first errorXACT_STATE() in the CATCH block
547 foreign key, XACT_ABORT ON-1, doomed. With XACT_ABORT on, nearly every error dooms the transaction.
547 foreign key, XACT_ABORT OFF1, committable. Only the statement failed; the CATCH could still write.
245 conversion, XACT_ABORT OFF-1, doomed anyway. Conversion failures (Error 245) end the transaction whatever the setting, so inside TRY the error is caught and the transaction is left doomed.
THROW in an AFTER trigger, XACT_ABORT OFF in the caller-1, doomed. Triggers run with XACT_ABORT on by default, so an error in a trigger dooms the caller’s transaction.
1205 deadlock victim-1, doomed, with @@TRANCOUNT still 1. The victim’s CATCH caught the 1205 with XACT_STATE() at -1 whether its XACT_ABORT was on or off, so the transaction is still open and can only be rolled back. Roll back before you retry. See Reading and Fixing a SQL Server Deadlock.
The trigger case, with XACT_ABORT OFF in the calling batchreal example output, column padding trimmed
xact_state error_number error_procedure ---------- ------------ ---------------- -1 50001 trg_Orders_Check Msg 3930, Level 16, State 1, Server HPAI01, 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, Server HPAI01, Line 1 Uncommittable transaction is detected at the end of the batch. The transaction is rolled back.

That is the common production shape: someone adds a validation trigger, and a procedure that has worked for years starts failing with 3930 instead of the trigger’s message. MS Docs lists the rules in SET XACT_ABORT.


Find the Error That Was Swallowed

Start with the cheap places. In SSMS, scroll the Messages tab above the 3930; if the CATCH selected or printed anything, the real error number is there. In the procedure, read the CATCH block: a write to a log table before the rollback is the usual culprit. If the error is intermittent or comes from an application, catch it as it happens with an Extended Events session on error_reported, filtered to the database:

CREATE EVENT SESSION find_first_error ON SERVER
ADD EVENT sqlserver.error_reported (
    ACTION (sqlserver.session_id, sqlserver.sql_text, sqlserver.database_name)
    WHERE severity >= 11 AND sqlserver.database_name = N'YourDb')
ADD TARGET package0.ring_buffer;

ALTER EVENT SESSION find_first_error ON SERVER STATE = START;

-- reproduce the failure, then read the buffer:
SELECT
    x.e.value('(@timestamp)[1]', 'datetime2(0)') AS event_time,
    x.e.value('(data[@name="error_number"]/value)[1]', 'int') AS error_number,
    x.e.value('(data[@name="message"]/value)[1]', 'nvarchar(300)') AS message
FROM (SELECT CAST(t.target_data AS xml) AS td
      FROM sys.dm_xe_session_targets t
      JOIN sys.dm_xe_sessions s ON s.address = t.event_session_address
      WHERE s.name = 'find_first_error' AND t.target_name = 'ring_buffer') r
CROSS APPLY r.td.nodes('RingBufferTarget/event') x(e)
ORDER BY event_time;

-- when you have it:
ALTER EVENT SESSION find_first_error ON SERVER STATE = STOP;
DROP EVENT SESSION find_first_error ON SERVER;
The session, after the failing procedure ran oncereal example output, time is UTC
event_time error_number message 2026-10-09 11:14:28 547 The INSERT statement conflicted with the FOREIGN KEY constraint "FK__Orders__Customer__4CA06362". The conflict occurred in database "zz_xact3930", table "dbo.Customer", column 'CustomerId'. 2026-10-09 11:14:28 3930 The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.

The row before the 3930 is the one you want. Two things that caught this on the lab: the XML query needs QUOTED_IDENTIFIER ON or it fails with Error 1934 (sqlcmd defaults it off; add -I), and on a server short of memory the XML shredding can sit waiting for a memory grant. Stop and drop the session when you are done. Extended Events Tracing has more sessions in the same style.


The CATCH Block That Causes It

This is the pattern behind most 3930 tickets. It looks careful: it logs the error, rolls back, and re-raises.

CREATE OR ALTER PROCEDURE dbo.AddOrder_Bad @CustomerId int, @Amount int
AS
SET XACT_ABORT ON;
BEGIN TRY
    BEGIN TRANSACTION;
    INSERT dbo.Orders (CustomerId, Amount) VALUES (@CustomerId, @Amount);
    COMMIT;
END TRY
BEGIN CATCH
    INSERT dbo.ErrorLog (ErrNo, ErrMsg) VALUES (ERROR_NUMBER(), ERROR_MESSAGE()); -- write while doomed
    IF @@TRANCOUNT > 0 ROLLBACK;
    THROW;
END CATCH;
EXEC dbo.AddOrder_Bad @CustomerId = 99, @Amount = 10real example output, not something to copy
Msg 3930, Level 16, State 1, Server HPAI01, Procedure dbo.AddOrder_Bad, Line 10 The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction. trancount errorlog_rows ----------- ------------- 0 0

The INSERT into the log table raised 3930, which ended the batch, so the ROLLBACK and the THROW never ran. The caller got 3930 instead of 547, and the error table that was meant to record the failure has nothing in it. The ordering is the whole bug.


The CATCH Pattern That Works

Save the error details into variables first, roll back if there is anything to roll back, write the log row outside the transaction, then re-raise the original:

CREATE OR ALTER PROCEDURE dbo.AddOrder_Good @CustomerId int, @Amount int
AS
SET XACT_ABORT ON;
BEGIN TRY
    BEGIN TRANSACTION;
    INSERT dbo.Orders (CustomerId, Amount) VALUES (@CustomerId, @Amount);
    COMMIT;
END TRY
BEGIN CATCH
    DECLARE @ErrNo int = ERROR_NUMBER(), @ErrMsg nvarchar(4000) = ERROR_MESSAGE();

    IF XACT_STATE() <> 0 ROLLBACK; -- covers both 1 and -1

    INSERT dbo.ErrorLog (ErrNo, ErrMsg) VALUES (@ErrNo, @ErrMsg); -- now outside the transaction
    THROW; -- the original error, number and line
END CATCH;
EXEC dbo.AddOrder_Good @CustomerId = 99, @Amount = 10real example output, column padding trimmed
Msg 547, Level 16, State 1, Server HPAI01, Procedure dbo.AddOrder_Good, Line 6 The INSERT statement conflicted with the FOREIGN KEY constraint "FK__Orders__Customer__4CA06362". The conflict occurred in database "zz_xact3930", table "dbo.Customer", column 'CustomerId'. trancount --------- 0 ErrNo ErrMsg 547 The INSERT statement conflicted with the FOREIGN KEY constraint "FK__Orders__Customer__4CA06362". The conflict occurred in database "zz_xact3930", table "dbo.Customer", column 'CustomerId'.

The caller now gets the real error with the line that raised it, the log row survives, and no transaction is left open. Two details matter:

  • Log after the rollback, not before. Even when the transaction is still committable (XACT_STATE() = 1), a log row written before the ROLLBACK is rolled back with it. On the lab, the XACT_ABORT OFF version logged the 547 and then rolled back, and the error table came out with 0 rows.
  • Use THROW, not RAISERROR with a new message. A bare THROW; in the CATCH re-raises the original error number, so monitoring and the application see 547, not a generic 50000.

If you deliberately keep partial work when the state is 1, test XACT_STATE() = 1 before the COMMIT and roll back on -1; never commit on @@TRANCOUNT alone. MS Docs: XACT_STATE and TRY…CATCH.


Two other messages travel with 3930 and point at the same problem.

3998 is raised when a batch ends with a doomed transaction still open, and SQL Server rolls it back for you. A CATCH that swallows the error and carries on produces 3998 with no 3930 at all:

SET XACT_ABORT OFF;
BEGIN TRY
    BEGIN TRANSACTION;
    INSERT dbo.Orders (CustomerId, Amount) VALUES (1, CAST('ten' AS int));
    COMMIT;
END TRY
BEGIN CATCH
    PRINT 'swallowed';
END CATCH;
SELECT 1 AS still_running;
A CATCH that swallows the errorreal example output, not something to copy
swallowed still_running ------------- 1 Msg 3998, Level 16, State 1, Server HPAI01, Line 1 Uncommittable transaction is detected at the end of the batch. The transaction is rolled back.

Everything the batch did after the swallowed error ran inside a transaction that was always going to roll back. That is the same family as the problems in Silent Failures.

266 is raised when a procedure returns with a different transaction count from the one it started with, typically a nested procedure that rolls back the caller’s transaction and returns quietly:

Outer BEGIN TRANSACTION, inner procedure rolls back and returns, outer COMMITreal example output, sqlcmd prints 266 as HResult 0x10A
HResult 0x10A, Level 16, State 2 Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0. trancount_after_exec -------------------- 0 Msg 3902, Level 16, State 1, Server HPAI01, Line 5 The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.

0x10A is 266 in hex; SSMS shows it as Msg 266 (documented, not reproduced here). A ROLLBACK anywhere rolls back the outermost transaction, so the caller’s COMMIT then fails with 3902. The fix is the same pattern: the procedure that rolls back should THROW, so the caller’s own CATCH deals with it, rather than return quietly.


What Not To Do

  • Do not take the message literally and add a ROLLBACK after the line that failed. The ROLLBACK has to come before any write in the CATCH, or the batch ends before it is reached.
  • Do not check @@TRANCOUNT to decide whether to commit. It is still nonzero for a doomed transaction.
  • Do not turn XACT_ABORT off to make 3930 go away. It only changes which errors doom the transaction. Conversion and trigger errors still do, and with it off a statement can fail while the rest of the transaction commits. Keep it on and write the CATCH properly.
  • Do not swallow errors in a CATCH. If you are not going to handle it, THROW it.

Frequently Asked Questions

Is my data half written after a 3930?
No. A doomed transaction cannot commit. On the lab it was rolled back when the batch ended: with Msg 3998 when XACT_ABORT was off, and with no extra message when it was on. Check @@TRANCOUNT in that session if you need to be sure nothing is still open.
Does a doomed transaction still hold locks?
Yes, until it is rolled back. A session that keeps running after a swallowed error holds them for the rest of the batch. Get Open Transactions shows sessions holding a transaction open.
Why does it only happen sometimes?
Because the first error depends on the data: a missing parent row, a bad value in a conversion, a deadlock. The 3930 is constant; the cause changes. The Extended Events session above catches it the next time it happens.
Can I read data while XACT_STATE() is -1?
Yes. Reads work, which is why the SELECT XACT_STATE() in the repro ran. Writes do not, so copy what you need into variables and write it after the rollback.
Should SET XACT_ABORT be ON or OFF?
On, for any procedure that opens a transaction. It makes the behaviour predictable (every error dooms the transaction, and, documented but not reproduced here, a client timeout cannot leave one open), and the CATCH pattern above handles the rest.

Where To Go Next

3930 is the symptom; these are the errors that usually cause it and the checks around it:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *