Back to Blog
C#

C# IAsyncDisposable for Async Resource Cleanup

c# iasyncdisposable: Learn how to implement IAsyncDisposable in C# to release resources asynchronously, with practical examples and common pitfalls.

async disposalresource managementawait usingIDisposable.NET
Illustration of an async disposal pattern in C# showing a resource being released with an await using statement.

When a resource needs asynchronous cleanup—like closing a network stream or flushing a buffer—the synchronous IDisposable pattern can block or leave work unfinished. The c# iasyncdisposable interface provides a standard way to release resources asynchronously. This article explains how to implement it, how to use it with await using, and where the pattern can go wrong.

What IAsyncDisposable Adds Over IDisposable

IDisposable has one method, Dispose(), which runs synchronously. That is fine for releasing memory or closing a handle, but it is insufficient when cleanup involves I/O. Flushing a stream, closing a database connection, or acknowledging a message broker all require asynchronous operations. If you call Dispose() and then block on an async operation inside it, you risk deadlocks and wasted thread pool threads. IAsyncDisposable defines a single method, DisposeAsync(), that returns a ValueTask and allows the cleanup work to be awaited naturally.

The interface looks like this:

public interface IAsyncDisposable { ValueTask DisposeAsync(); }

A class that implements IAsyncDisposable can be used with the await using statement, which ensures the cleanup runs asynchronously when the scope exits.

Implementing IAsyncDisposable Correctly

A minimal implementation of IAsyncDisposable is straightforward, but the details matter. The method should release the resource exactly once and avoid throwing exceptions unless the cleanup itself fails in a way the caller must handle.

Here is a class that wraps an asynchronous resource, such as a custom network connection:

public sealed class AsyncConnection : IAsyncDisposable { private readonly Stream _stream; private bool _disposed; public AsyncConnection(Stream stream) { _stream = stream; } public async ValueTask DisposeAsync() { if (_disposed) { return; } _disposed = true; await _stream.FlushAsync(); await _stream.DisposeAsync(); } }

This implementation marks the instance as disposed before performing the asynchronous work. That prevents a second call to DisposeAsync() from running the cleanup twice. The _disposed flag is not thread-safe, so concurrent disposal is still a problem, but that is rarely a requirement for a resource that is owned by a single consumer.

Using await using for Scoped Cleanup

The await using statement is the natural companion to IAsyncDisposable. It behaves like using, but it awaits the disposal at the end of the scope.

await using (var connection = new AsyncConnection(stream)) { // Use the connection }

When control leaves the block, the compiler generates a call to DisposeAsync() and awaits it. If the body of the block throws, the disposal still runs, just as it does with using. The syntax works with C# 8.0 and later, and it requires the project to target a runtime that supports IAsyncDisposable, such as .NET Core 3.0 or newer.

You can also use the newer declaration form:

await using var connection = new AsyncConnection(stream); // Use the connection

In this form, the disposal runs when the enclosing scope exits, which can be convenient but also means the resource lives longer than the block that uses it. Prefer the block form when you want to release the resource as soon as possible.

Combining IDisposable and IAsyncDisposable

Some resources have both synchronous and asynchronous cleanup paths. For example, a class might hold a CancellationTokenSource that must be disposed synchronously and a Stream that should be closed asynchronously. In that case, implement both interfaces and make Dispose() call the synchronous cleanup while DisposeAsync() performs the asynchronous cleanup.

public sealed class HybridResource : IDisposable, IAsyncDisposable { private readonly CancellationTokenSource _cts = new(); private readonly Stream _stream; private bool _disposed; public HybridResource(Stream stream) { _stream = stream; } public void Dispose() { if (_disposed) { return; } _disposed = true; _cts.Dispose(); _stream.Dispose(); } public async ValueTask DisposeAsync() { if (_disposed) { return; } _disposed = true; _cts.Dispose(); await _stream.DisposeAsync(); } }

A caller who uses using will get synchronous cleanup. A caller who uses await using will get asynchronous cleanup. The _disposed flag prevents both paths from running, which is important because a resource might be disposed by one pattern and then again by the other.

Handling Exceptions During Async Disposal

DisposeAsync() can throw, just like any other async method. The await using statement will propagate that exception, but it can mask an exception that was already thrown inside the using block. If the body throws and the disposal also throws, the disposal exception replaces the original one, which makes debugging harder.

To preserve the original exception, wrap the body in a try/catch and call DisposeAsync() explicitly:

var connection = new AsyncConnection(stream); try { // Use the connection } catch (Exception original) { try { await connection.DisposeAsync(); } catch (Exception disposal) { throw new AggregateException(original, disposal); } throw; } await connection.DisposeAsync();

This pattern is verbose, so only use it when the cleanup failure is likely and the original exception must be preserved. In most cases, await using is sufficient because the cleanup is simple and rarely fails.

Performance and Runtime Considerations

DisposeAsync() returns a ValueTask rather than a Task. That is a deliberate design choice. A ValueTask can avoid allocating a new object when the asynchronous operation completes synchronously, which is common for simple cleanup that does not actually need to yield. If the cleanup does need to await a real asynchronous operation, the ValueTask still works correctly.

When you implement DisposeAsync(), you should also return a ValueTask and use async only when you actually await something. If the method has no await, you can return ValueTask.CompletedTask instead of creating an unnecessary state machine.

Another consideration is that await using does not change the lifetime of the object itself. It only controls when DisposeAsync() is called. The object becomes eligible for garbage collection after the scope ends, but if it holds unmanaged resources, the asynchronous disposal is what releases them. Do not rely on the finalizer to call DisposeAsync(). Finalizers run on the finalizer thread and cannot safely perform asynchronous work.

Common Pitfalls and Edge Cases

One common mistake is to call DisposeAsync() from a finalizer or from a synchronous Dispose() method. That blocks the calling thread and can cause a deadlock if the async operation needs to resume on a synchronization context that is not available. If a class implements both interfaces, keep the async cleanup in DisposeAsync() and the synchronous cleanup in Dispose().

Another edge case is a resource that is shared across multiple consumers. IAsyncDisposable does not provide any reference counting. If two callers each hold the same instance, both may call DisposeAsync(), and the second call will see the _disposed flag. That is safe only if the flag is set before any asynchronous work begins, as shown in the earlier examples. For a genuinely shared resource, you need a different ownership model, such as a reference-counted wrapper.

Finally, consider what happens when the object is used after DisposeAsync() has been called. The class should throw ObjectDisposedException from any method that requires the resource to be alive. Without that guard, a caller might use a stream that has already been closed and receive a confusing error from the underlying API. Add a simple check at the start of each public method:

public async Task SendAsync(byte[] data) { ObjectDisposedException.ThrowIf(_disposed, this); await _stream.WriteAsync(data); }

This makes the object's state explicit and gives the caller a clear signal that the resource was already released.

c# iasyncdisposable: Practical Usage and Code Examples | RYUSLOG DEV