using statement versus declaration, lowering to try/finally, disposal order, pattern-based using for ref structs, and bugs with returned objects, deferred LINQ and Dispose exceptions.
using (var x = ...) { } and the C# 8 using declaration using var x = ...; both compile to try { ... } finally { x?.Dispose(); }. A declaration disposes at the end of the enclosing scope; several declarations are disposed in reverse order. For ref structs, using works with a pattern-based Dispose() method. Common bugs: returning or capturing an object that the using disposes, returning a lazy IEnumerable (LINQ or yield) built on a disposed resource, holding a resource longer than needed because a using declaration lives until the end of a long method, and an exception thrown by Dispose masking the original exception.
"
usingis just compiler sugar fortry/finallythat callsDispose. The classicusing (var x = ...) { }statement disposes at the end of its block; the C# 8using var x = ...;declaration disposes at the end of the enclosing scope, and multiple declarations are disposed in reverse order. It works forIDisposable, forref structs with a publicDispose()method, andawait usinghandlesIAsyncDisposable. The bugs I look for in code review are all lifetime bugs: returning or capturing something theusingwill dispose, returning a lazy query (LINQ,yield, unbuffered Dapper,IQueryable) built on a resource that is disposed before anyone enumerates it, ausing varthat keeps a pooled connection open through slow unrelated work, and aDisposethat throws and masks the original exception."
using var conn = new SqlConnection(cs);
await conn.OpenAsync();
return await conn.QuerySingleAsync<int>("select 1");
becomes, conceptually:
SqlConnection conn = new SqlConnection(cs);
try
{
await conn.OpenAsync();
return await conn.QuerySingleAsync<int>("select 1");
}
finally
{
if (conn != null) ((IDisposable)conn).Dispose();
}
| Detail | Behaviour |
|---|---|
null resource |
Allowed; nothing is disposed |
| Value types | Dispose called without boxing |
| The variable | Read-only: you can't reassign it inside the scope |
ref struct |
Pattern-based: any accessible void Dispose() works (C# 8+) |
await using |
Lowers to try/finally with await x.DisposeAsync() |
| Form | Disposed at | Best for |
|---|---|---|
using (var x = ...) { ... } |
End of the block | A short, explicit lifetime inside a longer method |
using var x = ...; |
End of the enclosing scope (method, loop body, block) | A resource used until the method ends |
Several using var |
Reverse declaration order | Dependent resources (writer over stream) |
| # | Bug | What goes wrong | Fix |
|---|---|---|---|
| 1 | Returning the resource (using var s = File.OpenRead(p); return s;) |
Caller gets a closed object | Don't using what you return; the caller owns it |
| 2 | Returning a lazy sequence (return reader.Lines().Where(...)) |
Enumerated after disposal → ObjectDisposedException / closed connection |
Materialise inside the scope, or make the whole method an iterator that owns the resource |
| 3 | Returning IQueryable from a disposed DbContext |
Query executes after the context is disposed | Execute (ToListAsync) inside the scope |
| 4 | Long-lived using var at the top of a long method |
Pooled connection/file held during unrelated slow work → pool exhaustion | using block with a narrow scope |
| 5 | Exception masking | Dispose throws in finally and replaces the real exception |
Dispose must not throw; provide explicit Flush/Commit |
| 6 | Plain using on async-capable types |
Sync disposal may block on I/O or skip async flush; IAsyncDisposable-only types won't compile |
await using in async code |
| 7 | Fire-and-forget inside the scope | Background task uses the resource after disposal | Await the task before the scope ends, or give it its own resource |
| 8 | Captured in a lambda run later (event handler, timer, Task.Run) |
Closure uses a disposed object | Don't capture scoped resources in deferred callbacks |
using scope? Is it still valid after disposal?IEnumerable/IQueryable/IAsyncEnumerable materialised or owning?using var holding a pooled resource longer than needed?IAsyncDisposable? Is await using used?Dispose throw?In our [orders service], a repository method opened using var connection and returned connection.Query<Order>(sql, buffered: false). The unbuffered Dapper query was enumerated by the caller after the method returned, so the connection was already disposed. Most requests happened to work during local testing because of timing and small results, but production logged intermittent InvalidOperationException: The connection is closed. I fixed the common path by materialising with .ToList() inside the scope. For the large export path I changed the API to an async iterator that owns the connection (await using inside an async IAsyncEnumerable<Order> method), so the connection lives exactly as long as the enumeration. I also added the pattern to our code review checklist.
The interviewer checks practical understanding of resource lifetimes in C#.
Green flags: lowering to try/finally, statement versus declaration scope, reverse disposal order, deferred execution bug, exception masking, await using, pattern-based using for ref structs.
Red flags: thinks using is only about memory; returns resources from using blocks; unaware of deferred execution issues.
using correctly// BUG: the reader is disposed when this method returns, before enumeration
static IEnumerable<string> PaidLazy(string path)
{
using var reader = new StreamReader(path);
return ReadLines(reader).Where(l => l.EndsWith(",Paid"));
}
// OK: the whole method is an iterator, so the using lives inside the state machine
static IEnumerable<string> PaidStreaming(string path)
{
using var reader = new StreamReader(path);
while (reader.ReadLine() is { } line)
if (line.EndsWith(",Paid")) yield return line;
}
In the iterator version, the compiler moves the using into the generated enumerator. The file opens on the first MoveNext() and is disposed when enumeration completes or when the enumerator is disposed (which foreach and LINQ operators like First() do). Two consequences:
GetEnumerator()/MoveNext() manually and never disposes the enumerator, the file leaks until finalisation.The async version works the same way:
public async IAsyncEnumerable<Order> StreamOrdersAsync([EnumeratorCancellation] CancellationToken ct = default)
{
await using var conn = new SqlConnection(_cs);
await conn.OpenAsync(ct);
await foreach (var o in conn.QueryUnbufferedAsync<Order>("select * from Orders").WithCancellation(ct))
yield return o;
}
using (var tx = new ThrowingOnDispose())
{
throw new InvalidOperationException("real problem");
}
// What propagates: the exception thrown by Dispose. "real problem" is lost.
In C#, an exception thrown from a finally block replaces the one in flight. That's why Dispose should not throw, and why APIs like transactions expose an explicit Commit()/CompleteAsync() that can fail loudly, while Dispose just rolls back quietly.
using var file = File.Create("out.gz");
using var zip = new GZipStream(file, CompressionLevel.Optimal);
using var writer = new StreamWriter(zip);
writer.Write("data");
// Disposed: writer (flush) → zip (write footer) → file (close)
If the order were reversed, the zip footer would be written to a closed file and the archive would be corrupt.
await using and ConfigureAwaitConfigureAwait(false) on an IAsyncDisposable returns a ConfiguredAsyncDisposable wrapper, which await using accepts. Because the wrapper is not the resource, declare the resource first:
var stream = File.OpenWrite(path);
await using (stream.ConfigureAwait(false))
{
await stream.WriteAsync(data).ConfigureAwait(false);
}
// Declaration form:
var file = File.OpenWrite(path);
await using var _ = file.ConfigureAwait(false);
await file.WriteAsync(data).ConfigureAwait(false);
Writing await using (var s = stream.ConfigureAwait(false)) compiles, but s is the wrapper, not the stream. This matters mainly in library code; ASP.NET Core has no synchronisation context.
ref structBefore C# 13, ref struct types couldn't implement interfaces, so C# 8 allowed using with any accessible void Dispose() on a ref struct. This is used by stack-based builders that rent buffers:
ref struct PooledBuffer
{
private byte[]? _array;
public PooledBuffer(int size) => _array = ArrayPool<byte>.Shared.Rent(size);
public Span<byte> Span => _array;
public void Dispose() { if (_array is { } a) { _array = null; ArrayPool<byte>.Shared.Return(a); } }
}
using var buf = new PooledBuffer(4096);
int count;
using (var conn = await _factory.OpenAsync(ct))
{
count = await conn.ExecuteScalarAsync<int>("select count(*) from Orders");
} // connection returned to the pool here
await _emailClient.SendReportAsync(count, ct); // slow, no connection held
With using var at the top, the connection would be held during the email call; under load that can exhaust the pool (Max Pool Size) and cause timeouts.
| Version | Change |
|---|---|
| C# 1 | using statement |
| C# 8 | using declarations, await using, pattern-based Dispose for ref struct |
| .NET Core 3.0 | IAsyncDisposable, IAsyncEnumerable<T> |
| C# 13 | ref struct can implement interfaces (pattern-based disposal still works) |
| .NET 10 / C# 14 | No changes to using semantics |
ObjectDisposedException; the materialised version works.PaidOrdersLazy as an iterator (yield return) and confirm it works and disposes when the foreach ends.Dispose throws and put it in a using around code that throws; observe which exception you catch.GZipStream/FileStream disposal manually and try to open the resulting .gz.Max Pool Size=5 on a SQL connection string, use using var at the top of a method with Task.Delay(2000) after the query, and run 20 concurrent calls; then narrow the scope and compare.GetEnumerator()/MoveNext() on the iterator version without disposing and check whether the file stays locked.File.WriteAllLines("orders.txt", ["1,Paid", "2,Pending", "3,Paid"]);
try
{
foreach (string line in PaidOrdersLazy("orders.txt")) Console.WriteLine(line);
}
catch (ObjectDisposedException ex)
{
Console.WriteLine($"Bug: {ex.GetType().Name} - the reader was disposed before enumeration");
}
foreach (string line in PaidOrders("orders.txt")) Console.WriteLine($"Fixed: {line}");
// BUG: query is deferred; the reader is disposed when the method returns
static IEnumerable<string> PaidOrdersLazy(string path)
{
using var reader = new StreamReader(path);
return ReadLines(reader).Where(l => l.EndsWith(",Paid"));
}
// FIX: materialise inside the scope (or make the whole method an iterator)
static List<string> PaidOrders(string path)
{
using var reader = new StreamReader(path);
return ReadLines(reader).Where(l => l.EndsWith(",Paid")).ToList();
}
static IEnumerable<string> ReadLines(StreamReader reader)
{
while (reader.ReadLine() is { } line) yield return line;
}
// Narrow scope: release the pooled resource before slow work
static async Task<int> CountThenProcessAsync(string path)
{
int count;
using (var reader = new StreamReader(path))
{
count = 0;
while (await reader.ReadLineAsync() is not null) count++;
} // file closed here
await Task.Delay(100); // slow work without holding the file open
return count;
}
usingWhat happens: return reader.Lines().Where(...), unbuffered Dapper, or IQueryable from a disposed DbContext.
Why it is wrong: enumeration happens after disposal → ObjectDisposedException or closed-connection errors, often intermittent.
Do instead: materialise inside the scope, or make the method an iterator that owns the resource.
What happens: using var stream = File.OpenRead(p); return stream;.
Why it is wrong: the caller receives a closed stream.
Do instead: don't using what you return; document that the caller owns it.
using var at the top of long methods for pooled resourcesWhat happens: the DB connection stays open during HTTP calls, sleeps or CPU work.
Why it is wrong: pool exhaustion and timeouts under load.
Do instead: narrow using blocks around the actual use.
using on async-disposable types in async codeWhat happens: using var writer = ... for a type with async flush.
Why it is wrong: sync disposal may block a thread on I/O or skip async work.
Do instead: await using.
Dispose that throwsWhat happens: Dispose performs a flush that can fail.
Why it is wrong: it masks the original exception from the using body.
Do instead: keep Dispose non-throwing; add an explicit Flush/Complete call.
What happens: _ = ProcessAsync(stream); then the using ends.
Why it is wrong: the task uses a disposed resource.
Do instead: await before the scope ends, or give the task its own resource.
What happens: a timer, event handler or Task.Run lambda captures a using variable.
Why it is wrong: the callback runs after disposal.
Do instead: don't capture scoped resources in callbacks that outlive the scope.
Dispose callsWhat happens: the stream is disposed before the writer/compressor over it.
Why it is wrong: buffered data or footers are lost; files are corrupt.
Do instead: let nested using declarations dispose in reverse order.
using frees memoryWhat happens: using is added around plain managed objects "to free memory".
Why it is wrong: Dispose releases resources, not managed memory; the GC still reclaims memory.
Do instead: use using for resources; reduce allocations for memory.
What happens: var e = seq.GetEnumerator(); e.MoveNext(); and then abandon it.
Why it is wrong: the iterator's using/finally never runs, leaking the resource.
Do instead: use foreach, or wrap manual enumeration in using var e = ....
using is simple syntax with lifetime consequences that show up in code review.
| Code smell | Likely incident |
|---|---|
| Lazy enumerable returned from using | Intermittent disposed-object errors |
| Long-lived using declaration | Connection pool exhaustion under load |
| Background task using a scoped resource | Errors after the request ends |
Dispose that can throw |
Lost root-cause exceptions in logs |
Seniors review the lifetime, not just the presence, of using.
IEnumerable<T> is just a promise that you can get an enumerator. IEnumerator<T> gives MoveNext, Current and Dispose, and that is all: no count, no indexing, and possibly lazy, infinite or single-use. foreach is pattern-based and compiles to a while (MoveNext()) loop with Dispose in a finally. List<T> uses a struct enumerator, which gets boxed if you iterate through the interface. IAsyncEnumerable<T> is the async version, used with await foreach. I accept IEnumerable<T> when I only iterate once, and return IReadOnlyList<T> unless I mean to stream.
Implement IAsyncDisposable when cleanup involves asynchronous I/O, such as flushing a buffered stream, sending a final message or closing a network connection gracefully. DisposeAsync() returns ValueTask. await using var x = ...; compiles to try/finally with await x.DisposeAsync(), and you can write await using (x.ConfigureAwait(false)) in library code. A type that supports both should implement IDisposable and IAsyncDisposable, put async cleanup in protected virtual ValueTask DisposeAsyncCore(), and call GC.SuppressFinalize. In DI, use CreateAsyncScope(): disposing a provider or scope synchronously throws when it holds a service that implements only IAsyncDisposable.
Implement IDisposable when your type owns something that must be released deterministically: an unmanaged resource (usually via SafeHandle) or another IDisposable. For a sealed class, a simple Dispose() that disposes owned fields is enough. For unsealed classes use the pattern Dispose() calling protected virtual Dispose(bool disposing) and GC.SuppressFinalize(this). Make Dispose idempotent and non-throwing, and throw ObjectDisposedException (ObjectDisposedException.ThrowIf) from members used after disposal. The owner disposes: whoever creates an object disposes it, unless ownership is explicitly transferred. The DI container disposes what it creates, at the end of the scope or the app.
How to tell it (STAR)
We had intermittent closed-connection errors. I found a repository returning a lazy query from inside a using scope, so the caller enumerated after disposal. Materialising inside the scope fixed it, and for big exports I switched to an async iterator that owns the connection.
Last updated 31 Oct 2026.