What it means
SQL Server notes @@TRANCOUNT, the number of open BEGIN TRANSACTIONs on the connection, when a
stored procedure starts and again when it ends. If the two differ, it raises error 266 as the
procedure returns:
Msg 266, Level 16, State 2, Server 7732422b7f56, Procedure seo_err_mssql.inner_rollback, Line 4
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
sqlcmd 18 sometimes prints it as HResult 0x10A, Level 16, State 2 instead: 0x10A is 266 in
hexadecimal.
The two counts tell you which way it went wrong:
- Previous 0, current 1: the procedure began a transaction and returned without committing or rolling it back. The transaction is still open on your connection, holding its locks, and the procedure’s changes aren’t committed. Whatever runs next on that connection is part of it.
- Previous 1, current 0: you called the procedure inside your own transaction, and it ran
ROLLBACK. In SQL Server aROLLBACKwithout a savepoint name always undoes the outermost transaction, so your work before the call is gone as well, and your ownCOMMITlater fails with error 3902.
The procedure’s statements did run; 266 only reports the count. What happened to the data depends on which case it is.
Common causes
- A path that skips the
COMMIT: an earlyRETURN, aGOTO, or anIFbranch added later. - An error path with no
ROLLBACK. ACATCHblock that logs and returns, leaving the transaction open. ROLLBACKin a procedure that may be called inside a transaction. NestedBEGIN TRANSACTIONonly increments a counter: the innerCOMMITdecrements it, the innerROLLBACKundoes everything.SET XACT_ABORT ONwith an error inside a nested call. The error rolls back the whole transaction, so the caller seescurrent count = 0along with the original error.
How to fix it
Commit or roll back on every path
Wrap the body in TRY … CATCH, commit at the end of TRY, and roll back in CATCH before re-raising:
CREATE OR ALTER PROCEDURE dbo.close_order @order_id int AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
UPDATE dbo.orders SET status = 'closed' WHERE id = @order_id;
INSERT INTO dbo.order_events (order_id, event) VALUES (@order_id, 'closed');
COMMIT;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
END;
Never RETURN between BEGIN TRANSACTION and COMMIT.
Use a savepoint when there may be an outer transaction
If callers sometimes wrap the procedure in their own transaction, start one only when there isn’t one, and otherwise mark a savepoint, so that a failure undoes only the procedure’s work:
CREATE OR ALTER PROCEDURE dbo.set_total @order_id int, @total decimal(10, 2) AS
BEGIN
SET NOCOUNT ON;
DECLARE @trancount int = @@TRANCOUNT;
BEGIN TRY
IF @trancount = 0 BEGIN TRANSACTION;
ELSE SAVE TRANSACTION set_total;
UPDATE dbo.orders SET total = @total WHERE id = @order_id;
IF @trancount = 0 COMMIT;
END TRY
BEGIN CATCH
IF XACT_STATE() = -1 ROLLBACK;
ELSE IF XACT_STATE() = 1 AND @trancount = 0 ROLLBACK;
ELSE IF XACT_STATE() = 1 AND @trancount > 0 ROLLBACK TRANSACTION set_total;
THROW;
END CATCH;
END;
XACT_STATE() of -1 means the transaction is doomed and can only be rolled back entirely (see
error 3930). With XACT_ABORT ON, every error
dooms it, so this pattern leaves XACT_ABORT off.
Close a transaction that was left open
If a session already has one open, find it and end it from that session, with COMMIT if its
changes should stay or ROLLBACK if not:
SELECT st.session_id, s.login_name, s.program_name, t.transaction_begin_time
FROM sys.dm_tran_session_transactions AS st
JOIN sys.dm_tran_active_transactions AS t ON t.transaction_id = st.transaction_id
JOIN sys.dm_exec_sessions AS s ON s.session_id = st.session_id
ORDER BY t.transaction_begin_time;
Seeing other sessions needs VIEW SERVER STATE (VIEW SERVER PERFORMANCE STATE on SQL Server 2022
and later). Ending a session with KILL rolls back its open transaction.
Reproduce it
On SQL Server 2022 (16.0.4295.3) with sqlcmd, a procedure that begins a transaction, updates a row
and RETURNs before its COMMIT:
EXEC seo_err_mssql.leaky_begin;
SELECT @@TRANCOUNT AS trancount_after;
HResult 0x10A, Level 16, State 2
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 0, current count = 1.
trancount_after
---------------
1
(We then rolled that transaction back by hand.) A procedure that ends with ROLLBACK, called inside
a transaction, with SET NOCOUNT ON:
BEGIN TRANSACTION;
EXEC seo_err_mssql.inner_rollback;
COMMIT;
Msg 266, Level 16, State 2, Server 7732422b7f56, Procedure seo_err_mssql.inner_rollback, Line 4
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
Msg 3902, Level 16, State 1, Server 7732422b7f56, Line 4
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
sqlcmd printed 266 in either form from run to run; the number, state and text were the same. The
query above, run while a transaction was open, listed it with its session, login and start time. With
the savepoint procedure above, called inside a transaction that had already renamed a customer, a
CHECK constraint failure inside the procedure was caught by the caller with @@TRANCOUNT 1 and
XACT_STATE() 1, and the rename was still there: no 266. SQL Server 2019 and 2025 printed the same
messages.
In Inlet
Inlet’s Activity view lists sessions and who blocks whom: a session that left a transaction open,
holding locks others need, shows up as the one blocking them, and you can end it there (KILL,
Pro). It needs VIEW SERVER STATE (VIEW SERVER PERFORMANCE STATE from SQL Server 2022). When a procedure call
fails this way, Inlet shows the message with SQL Server error 266, state 2, severity 16 and links
to this page.