Quick answer for advanced users
You tried to roll back a transaction that was already committed. In C#, move the SqlTransaction.Rollback() call inside a catch block and ensure Commit() is only called after all operations succeed. In T-SQL, check @@TRANCOUNT before rolling back nested transactions.
I've seen this error pop up more times than I care to count, especially when folks are using explicit transactions in SQL Server 2019 or 2022 with C# code that mixes try-catch blocks with nested transaction logic. The core issue is simple: your application's transaction state machine got out of sync. You called Commit somewhere, then later—maybe in an error handler or cleanup routine—another part of the code tried to Rollback the same transaction. SQL Server slaps you with 0XC0190016 to say "sorry, that ship has sailed."
Why this happens
The most common real-world trigger is a C# method that starts a SqlTransaction, runs multiple SQL commands, and then somewhere in a finally block or a generic error handler, it calls transaction.Rollback() without checking if the transaction is still active. Another scenario is using nested transactions with SAVE TRAN in T-SQL, where an inner transaction gets committed but an outer rollback tries to undo it. I fought this error for three hours once because I had a stored procedure that used BEGIN TRAN inside a loop—when one iteration committed, the next loop's error handling tried to roll back the wrong scope.
Fix steps (C# with SqlTransaction)
- Restructure your try-catch-finally blocks. Only call
Rollback()inside acatchblock, never infinally. Thefinallyblock runs even after a successful commit, which causes this error.using (var connection = new SqlConnection(connectionString)){ connection.Open(); using (var transaction = connection.BeginTransaction()) { try { // Execute your commands here var command = new SqlCommand("UPDATE Accounts SET Balance = Balance - 100 WHERE AccountId = 1", connection, transaction); command.ExecuteNonQuery(); // If all succeeds, commit transaction.Commit(); } catch { // Only roll back on failure transaction.Rollback(); throw; // Re-throw or handle appropriately } }} - Check transaction state before rollback in cleanup code. If you must use a
finallyblock for resource cleanup, check if the transaction is still active using a flag ortransaction.Connection != null. I prefer a boolean flag set tofalseafter commit.bool committed = false;try{ // ... do work ... transaction.Commit(); committed = true;}catch{ transaction.Rollback(); throw;}finally{ if (!committed && transaction.Connection != null) { transaction.Rollback(); }}
Alternative fix (T-SQL nested transactions)
If you're writing stored procedures and see this error inside SQL Server itself, you're likely dealing with nested BEGIN TRAN statements. The trick is to only roll back the outermost transaction and use SAVE TRAN for inner savepoints.
BEGIN TRY BEGIN TRANSACTION; -- Inner savepoint instead of nested transaction SAVE TRANSACTION SavePoint1; BEGIN TRY -- Some risky operation UPDATE Accounts SET Balance = Balance - 100 WHERE AccountId = 1; END TRY BEGIN CATCH -- Roll back to savepoint, not whole transaction ROLLBACK TRANSACTION SavePoint1; -- Handle error, maybe log it END CATCH -- More operations COMMIT TRANSACTION;END TRYBEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; -- Re-throw or log errorEND CATCHNotice I check @@TRANCOUNT before rolling back. That's the safety net that stops 0XC0190016 cold.
Prevention tips
- Never call Rollback() in a finally block. This is the number one cause. Use the try-catch pattern above.
- Avoid explicit nested transactions. Use
SAVE TRANinstead. SQL Server doesn't truly support nested transactions—eachBEGIN TRANincrements a counter, but only the finalCOMMITactually writes to disk. Rollback of any level rolls back everything. - Use a transaction scope library. In .NET,
TransactionScopehandles commit/rollback state automatically, which sidesteps this exact bug. Just wrap your code inusing (var scope = new TransactionScope())and callscope.Complete()only when successful. - Log transaction state in development. Add debug prints showing
@@TRANCOUNTbefore and after each COMMIT/ROLLBACK. It's tedious, but it catches these state mismatches fast.
When the fix doesn't work
If you've restructured your code and still see the error, check for multiple threads accessing the same transaction object. SqlTransaction isn't thread-safe. One thread might commit while another attempts a rollback. Also look for event handlers or callbacks that fire asynchronously—they can trigger rollback after your main code path already committed. I once had a SqlInfoMessageEventHandler that was rolling back transactions in the background. Took me two days to trace that one.
That's the full picture. Fix the state machine, and 0XC0190016 will stop haunting your logs.