async programming
TransactionScope
C#
cancellation
resource management

How to dispose TransactionScope in cancelable async/await?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

TransactionScope should still be disposed with a normal using block even inside cancelable async code. The key rule is that cancellation does not require a special dispose path. If the scope is disposed without Complete() having been called, the transaction is rolled back. The async-specific detail is that you must create the scope with TransactionScopeAsyncFlowOption.Enabled so the ambient transaction can flow across await points.

Use using, Not a Special Cancellation Pattern

TransactionScope implements IDisposable, not IAsyncDisposable, so the disposal pattern is the same as in synchronous code.

csharp
using var scope = new TransactionScope(
    TransactionScopeAsyncFlowOption.Enabled);

When execution leaves the using block, Dispose() runs automatically whether the method completed normally, threw an exception, or observed cancellation.

The important semantic rule is:

  • call Complete() only when the transaction should commit
  • if Complete() is never called, disposal causes rollback

That means canceled work should usually just exit without calling Complete().

A Correct Async Example With Cancellation

csharp
1using System;
2using System.Threading;
3using System.Threading.Tasks;
4using System.Transactions;
5
6public static async Task SaveAsync(CancellationToken cancellationToken)
7{
8    using var scope = new TransactionScope(
9        TransactionScopeOption.Required,
10        new TransactionOptions
11        {
12            IsolationLevel = IsolationLevel.ReadCommitted
13        },
14        TransactionScopeAsyncFlowOption.Enabled);
15
16    await Task.Delay(100, cancellationToken);
17    cancellationToken.ThrowIfCancellationRequested();
18
19    // Perform database work here.
20    await Task.Delay(100, cancellationToken);
21
22    scope.Complete();
23}

If cancellation is requested before scope.Complete(), the exception escapes, the using block ends, and the transaction is rolled back during Dispose().

Why TransactionScopeAsyncFlowOption.Enabled Matters

Without async flow enabled, the ambient transaction does not flow reliably across await, and you can get runtime errors or inconsistent behavior.

This is the async-safe construction pattern:

csharp
var scope = new TransactionScope(
    TransactionScopeOption.Required,
    TransactionScopeAsyncFlowOption.Enabled);

Or with explicit transaction options:

csharp
1var scope = new TransactionScope(
2    TransactionScopeOption.Required,
3    new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
4    TransactionScopeAsyncFlowOption.Enabled);

If you are mixing TransactionScope with await, this option is not optional.

Cancellation and Rollback Are Separate Ideas

A CancellationToken does not automatically “tell the transaction to roll back.” What actually happens is simpler:

  1. your code observes cancellation
  2. your code stops before calling Complete()
  3. disposal happens
  4. the uncompleted scope rolls back

That is why rollback is a result of normal TransactionScope semantics, not a special cancellation hook.

Catching OperationCanceledException

If you catch cancellation inside the method, be careful not to accidentally call Complete() afterward.

csharp
1public static async Task SaveAsync(CancellationToken cancellationToken)
2{
3    using var scope = new TransactionScope(
4        TransactionScopeAsyncFlowOption.Enabled);
5
6    try
7    {
8        await Task.Delay(100, cancellationToken);
9        cancellationToken.ThrowIfCancellationRequested();
10
11        // database work
12
13        scope.Complete();
14    }
15    catch (OperationCanceledException)
16    {
17        // No Complete call here. Disposal will roll back.
18        throw;
19    }
20}

You do not need a manual rollback call. Avoid Complete(), let the exception propagate if appropriate, and let disposal handle the rollback.

Keep the Transaction Scope Small

Long-running async flows inside a transaction are rarely ideal. The longer the transaction stays open, the more likely you are to create lock contention, timeouts, and operational pain.

A practical rule is:

  • keep the transactional section short
  • do cancelable non-transactional work before opening the scope when possible
  • open the scope only around the operations that truly must commit atomically

That design matters more than any disposal trick.

When Not to Use TransactionScope

If your async workflow crosses multiple services, external HTTP calls, or long-running background work, TransactionScope is often the wrong abstraction. In those cases, explicit database transactions or application-level compensation logic are usually a better fit.

The more distributed and long-lived the workflow becomes, the less attractive ambient transaction scope tends to be.

Common Pitfalls

A common mistake is forgetting TransactionScopeAsyncFlowOption.Enabled, which breaks async transaction flow.

Another issue is catching OperationCanceledException and then continuing into a code path that still calls Complete(). That turns a canceled operation into a committed transaction.

Developers also sometimes assume they need a manual rollback API. For TransactionScope, simply disposing without Complete() is the rollback path.

Finally, long-lived async operations inside a transaction are often a design problem even when the code is technically correct.

Summary

  • Dispose TransactionScope with an ordinary using block, even in cancelable async code.
  • Enable async flow with TransactionScopeAsyncFlowOption.Enabled.
  • If cancellation occurs before Complete(), disposal rolls the transaction back.
  • Do not call Complete() on canceled or failed work.
  • Keep the transactional section as short as possible.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.