Download

Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements

A stored procedure ended with a different number of open transactions than it started with. “Previous count = 0, current count = 1” means it left one open; “Previous count = 1, current count = 0” means its ROLLBACK undid the caller’s transaction too. Commit or roll back on every path, and use a savepoint when called inside a transaction.

SQL Server error 266· Tested on SQL Server 2022 RTM-CU27 (16.0.4295.3); same messages on 2019 RTM-CU32-GDR and 2025 RTM-CU9· Updated 11 October 2026

Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 0, current count = 1.

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 a ROLLBACK without a savepoint name always undoes the outermost transaction, so your work before the call is gone as well, and your own COMMIT later 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

  1. A path that skips the COMMIT: an early RETURN, a GOTO, or an IF branch added later.
  2. An error path with no ROLLBACK. A CATCH block that logs and returns, leaving the transaction open.
  3. ROLLBACK in a procedure that may be called inside a transaction. Nested BEGIN TRANSACTION only increments a counter: the inner COMMIT decrements it, the inner ROLLBACK undoes everything.
  4. SET XACT_ABORT ON with an error inside a nested call. The error rolls back the whole transaction, so the caller sees current count = 0 along 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.

Inlet: a database client for the Mac

One native app for PostgreSQL, MySQL, SQL Server, SQLite, MongoDB and Redis. It explains errors where they happen, holds your edits until you save them, and keeps production read-only until you say so.

Version 0.1.0 · macOS 26 Tahoe or later · Apple silicon and Intel