XACT_E_NOTRANSACTION (0X8004D00E) — transaction already done
Kicks in when you try to commit or rollback a transaction that's already finished. Usually from nested transaction mishandling or misordered API calls.
When this error hits
You're running a distributed transaction — maybe across two SQL Server instances via a linked server, or a .NET TransactionScope that spans multiple databases. Everything's been fine for months. Then suddenly you get XACT_E_NOTRANSACTION (0x8004D00E) with the message "The transaction has already been implicitly or explicitly committed or aborted."
The exact trigger? Almost always a nested transaction where an inner commit or rollback finishes the outer transaction before you expect it. I've seen it happen when a stored procedure calls another proc that has its own BEGIN TRANSACTION / COMMIT without checking the transaction count. Or when a .NET TransactionScope is disposed twice because of an exception in a using block.
Root cause
At its core, this error means you tried to call Commit() or Rollback() on a transaction that's already done. SQL Server and MSDTC track transaction state — once a transaction is committed or aborted, any further calls to manipulate that same transaction fail with this exact error code.
The usual suspects:
- Nested transactions in T-SQL. SQL Server doesn't support true nested transactions. An inner
COMMITdecrements@@TRANCOUNT. If it drops to zero, the outer transaction commits too. Then when the outer code tries to commit, it finds nothing to commit. - Misordered transaction scope in .NET. You wrap a
TransactionScopein ausingblock, but somewhere inside you manually callComplete()on the underlyingCommittableTransaction. Then the scope'sDispose()tries to commit again — boom. - MSDTC timeout. The distributed transaction coordinator times out and aborts the transaction while your app is still running. Your next
Commit()call sees no active transaction. - Linked server transactions. A remote proc on the linked server commits the distributed transaction, but the local code still has the original transaction object. When it tries to commit locally, it fails.
How to fix it
- Check your T-SQL nested transactions.
Run this in SQL Server Management Studio on the database where the transaction runs:
before and after each commit. If it goes to zero unexpectedly, you've got a nested commit killing your outer transaction. Fix by usingSELECT @@TRANCOUNTSAVE TRANinstead ofBEGIN TRANin inner stored procedures, or restructure them to not start their own transactions. - Inspect .NET TransactionScope usage.
Never manually callComplete()on the transaction object. Only callTransactionScope.Complete()once, before theusingblock ends. If you useTransactionScopeAsyncFlowOption.Enabled, make sure the scope isn't disposed on a different thread. Here's a solid pattern:
If an exception happens, don't callusing (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { // do work scope.Complete(); }Complete()— let theusingblock dispose and rollback. - Increase MSDTC timeout.
Open Component Services (comexp.msc). Go to Component Services > Computers > My Computer > Distributed Transaction Coordinator > Local DTC. Right-click, select Properties. On the Transactions tab, increase the Transaction Timeout (default is 60 seconds). Set it to 120 or 300 if your transactions are long-running. Also check network latency — high ping between servers can trigger premature timeouts. - Verify linked server configuration.
If the error comes from a linked server query, check the linked server's RPC settings. In SQL Server Management Studio, right-click the linked server > Properties > Server Options. SetEnable Promotion of Distributed Transactionsto False if the remote proc doesn't need to participate in the distributed transaction. This prevents the MSDTC from promoting the local transaction to a distributed one. But only do this if you're sure the remote work doesn't require atomicity with the local transaction. - Add explicit error handling in stored procedures.
Wrap each stored procedure's transaction logic withBEGIN TRY...BEGIN CATCH. Check@@TRANCOUNTbefore committing. If it's zero, skip the commit. Example:
This prevents the "commit on empty transaction" scenario.BEGIN TRY BEGIN TRANSACTION; -- do work IF @@TRANCOUNT > 0 COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; THROW; END CATCH - Monitor MSDTC logs.
When the error occurs, check Windows Event Viewer > Applications and Services Logs > Microsoft > Windows > MSDTC Client. Look for events with ID 4099 or 4100 — those signal timeout or abort decisions. They'll tell you exactly which resource manager killed the transaction.
Still failing? Check these
If you've done all the above and the error persists, look at these less common causes:
- Firewall blocking MSDTC ports. MSDTC uses dynamic port ranges by default (ports 1024-1034 in some configs, or 49152-65535 in others). If a firewall between servers is dropping traffic, the transaction can abort silently. Pin MSDTC to a fixed port range in the DTC properties to make firewall rules easier.
- Incompatible SQL Server versions. I've seen this error when a 2012 instance tries to distribute a transaction to a 2008 R2 linked server. The older server's MSDTC implementation doesn't handle new transaction flags. Upgrade the linked server or change the transaction isolation level to
READ COMMITTED. - Application code calling Commit() inside a callback. Some ORMs (Entity Framework, NHibernate) have event hooks after a transaction commits. If your code tries to commit again in that hook, you get this error. Don't do that.
- Deadlock victim rolls back the transaction. When SQL Server kills a deadlock victim, it aborts the transaction. Your application's next commit call fails with this error. Handle deadlock retries by catching deadlock exceptions (error 1205) and replaying the whole transaction.
The bottom line: This error is almost never a DTC config problem. It's a code problem. Look at how you're managing transaction nesting and scope in your application and stored procedures. Fix that, and 0x8004D00E disappears.
Was this solution helpful?