Why C# Has IDisposable When It Already Has a Garbage Collector
What IDisposable actually is in C#, and how to know you understood it
When your code starts running, objects are created, methods are called, and data appears in memory. As the program continues, some of those things are no longer needed. The Garbage Collector eventually comes along and clears that unused managed memory. IDisposable has a different job.
Some objects do more than simply occupy memory. They may temporarily borrow something from the operating system or from another part of the program. A file can be opened or a database connection can be taken from a pool for example.
Those things should often be returned as soon as your code is finished with them. That’s where IDisposable comes in.
The Garbage Collector asks
Is this object still needed in memory?
IDisposable is about a different question
Is this object still holding something that should be released now?
So while the Garbage Collector cleans up memory later, Dispose() is used when your code knows that a resource is no longer needed and should be released immediately.
Outside Ordinary Managed Memory
You need to ask yourself whether this object only contains ordinary C# data, or whether it is also holding something that must be closed, returned, stopped, or released.
A FileStream may represent an open file.
A SqlConnection may represent a database connection.
A socket may represent an active network connection.
A timer may still be running.
A useful rule of thumb is this: If the object only stores normal managed data such as numbers, strings, collections, or references to other ordinary managed objects, the Garbage Collector can usually take care of it.
If the object represents access to something scarce, external, shared, or still active, you should check whether it needs explicit cleanup.
Compare these two objects.
var customer = new Customer();var stream = File.OpenRead("report.txt");Both variables refer to C# objects. Both objects occupy managed memory. But the second object has another relationship.
customer
↓
Customer object in managed memoryThe Customer object may simply contain values such as a name, an address, or an identifier.
When nothing refers to that object anymore, the Garbage Collector can eventually reclaim its memory.
The stream is different.
stream
↓
FileStream object in managed memory
↓
Operating-system file handle
↓
report.txtThe FileStream exists in managed memory, but it also represents access to something managed outside the normal C# heap.
That second relationship is why simply waiting for the object’s memory to be reclaimed isn’t always enough.
Another practical clue is the type itself.
If it implements IDisposable or IAsyncDisposable, that is the type telling you that its lifetime matters and that something should usually happen when you are finished with it.
That is where IDisposable becomes important.
IDisposable is about resources your code has temporarily borrowed and should return when it is finished with them.
You clean something up when your code is done with something that is limited, external, shared, or temporary.
When, Where, What, and How?
There are four useful questions to ask whenever you see a disposable object.
What needs cleaning up?
First, identify the resource.
var stream = File.OpenRead("report.txt");File.OpenRead() returns a FileStream object managed by .NET. That object also corresponds to an open file managed by the operating system.
Conceptually, the relationship looks like this.
C# FileStream object
↓
Operating-system file handle
↓
report.txt is openThe object in managed memory is only part of the story. If you are finished reading the file, you want the file handle released.
In practical terms, that means telling the operating system that your program no longer needs to keep that file open.
The file is then free to be accessed, changed, moved, or deleted by other code or programs, depending on how it was opened.
When should it be cleaned up?
As soon as your code is finished using it. Not when the Garbage Collector happens to run. Not when the process eventually exits. When your own logic knows the resource is no longer needed.
Dispose() tells the object
We are finished. Release what you are holding.
The important word is now.
Calling Dispose() does not mean that the C# object immediately disappears from memory.
The object itself may still exist, and references to it may still exist. Disposal ends the resource-owning part of its lifetime. The Garbage Collector is still responsible for reclaiming the managed memory later.
So these are two separate events.
Dispose()
↓
Release the resource nowGarbage Collector
↓
Reclaim the object's managed memory laterWhere should cleanup happen?
Ideally, at the boundary where ownership ends. If a method creates a resource and only needs it inside that method, the cleanup usually belongs there too.
When the method’s scope ends, the stream is disposed. That keeps the lifetime of the resource close to the code that uses it.
How do we make sure cleanup happens?
C# uses the word using for two different things, so it helps to separate them first.
At the top of a file, you may see
using System.IO;Here, using simply lets you use types from System.IO without writing their full names.
But inside a method, using can mean something completely different.
using var stream = File.OpenRead("report.txt");Here, using tells C#
This object needs cleanup. When this scope ends, call its
Dispose()method.
In this example, File.OpenRead() opens report.txt and returns a FileStream.
Your code can use that stream while the method is running. When the method or block ends, C# calls
stream.Dispose();That releases the file handle and tells the operating system that your program is finished with the file.
So this line
using var stream = File.OpenRead("report.txt");can be read as
Open this file, let me use it here, and make sure it is released when I leave this scope.
The important part is that using does not perform the cleanup itself. Dispose() performs the cleanup. using makes sure Dispose() is called at the right moment.
It also protects you if something goes wrong.
using var stream = File.OpenRead("report.txt");ReadData(stream);If ReadData() throws an exception, C# still disposes the stream when execution leaves the scope.
Without using, a manually written stream.Dispose() later in the method might never be reached.
A useful way to understand why this works is to look at what using roughly saves you from having to write yourself.
Get coding | seaphylla | omnivorous’s stories in your inbox
Join Medium for free to get updates from this writer.
This
using var stream = File.OpenRead("report.txt");ReadData(stream);is conceptually similar to
var stream = File.OpenRead("report.txt");try
{
ReadData(stream);
}
finally
{
stream.Dispose();
}A finally block runs when execution leaves the try block, even if an exception occurs.
That is why using is so useful. It connects the lifetime of the resource to the scope of the code using it.
Resource is opened
↓
Code uses it
↓
Scope ends normally
or because of an exception
↓
C# calls Dispose()
↓
Resource is releasedThat is how using helps make cleanup reliable. You don’t need to write disposal behavior for every project or every time you use IDisposable.
Existing .NET classes already contain their own Dispose() implementations. Your job is only to make sure that Dispose() gets called, by using using.
You write your own Dispose() method only when you create a class that owns resources which themselves need cleanup.
In that case, your class implements IDisposable and its Dispose() method passes the cleanup on to the resources it owns.
What Is IDisposable Itself?
IDisposable is just an interface. Its basic shape is simple.
public interface IDisposable
{
void Dispose();
}A class that implements it makes a promise.
public class Example : IDisposable
{
public void Dispose()
{
// Cleanup happens here
}
}The promise is essentially this.
This object has an explicit end to its useful lifetime.
The interface does not perform cleanup by itself.
Your implementation of Dispose() does. And using gives the caller a convenient and reliable way to make sure that method is called.
Your own class gives Dispose() meaning by deciding what resources it owns and what should happen to them when the object is finished.
So the name Dispose is part of the .NET contract, but the actual behavior is written in the implementation.
What Happens If You Do Not Clean Up?
The consequences depend on the resource. If you leave a file open, another process may be unable to modify or delete it.
If you keep database connections checked out, the connection pool may eventually run out.
If you leave sockets active, your application can exhaust network resources.
If timers keep running, they may continue executing code after you expected them to stop.
None of those problems necessarily look like a classic memory leak. That is what makes them easy to misunderstand. Imagine a database pool with ten connections.
[1] [2] [3] [4] [5]
[6] [7] [8] [9] [10]A request borrows one connection. It performs a query. Then it returns the connection. The same ten connections can serve thousands of requests.
Now imagine the code keeps borrowing connections but never returns them.
Request 1 → connection 1
Request 2 → connection 2
Request 3 → connection 3
...
Request 10 → connection 10Request 11 arrives.
There may still be gigabytes of free RAM. The Garbage Collector may be doing its job perfectly. But the pool is empty. The application waits or times out. That is why IDisposable is not really about memory. It is about resource lifetime.
Ownership Matters
There is one more rule that prevents a different kind of bug. Don’t dispose something simply because it implements IDisposable. Ask who owns it.
Suppose your method receives a stream.
void Read(Stream stream)
{
// Read data
}Your method didn’t create the stream. The caller may want to continue using it afterwards.
using var stream = File.OpenRead("report.txt");Read(stream);DoSomethingElse(stream);
If Read() disposes the stream, then DoSomethingElse() receives an already closed stream.
That is why a useful default rule is
The code that creates or acquires a disposable resource is usually responsible for disposing it.
There are exceptions, but ownership should always be clear.
The important lesson is not
Dispose everything you see.
It is
Dispose the resources you own when their useful lifetime ends.
What About await using?
Some cleanup operations are asynchronous.
A resource may need to flush data, finish network communication, or perform another asynchronous shutdown operation.
For those cases C# has IAsyncDisposable.
Instead of
using var resource = ...;you may see
await using var resource = ...;The idea is the same. The only difference is that the cleanup itself may need to be awaited.
using
↓
Synchronous cleanupawait using
↓
Asynchronous cleanupWhat About Finalizers?
C# also has finalizers.
~Example()
{
}They may run when the Garbage Collector processes an object. But they are not a replacement for timely disposal.
You do not know exactly when the finalizer will run.
That makes a finalizer unsuitable as the normal mechanism for resources that should be released as soon as your code has finished with them.
It is also important not to assume that every class implementing IDisposable needs a finalizer.
Most don’t.
If your class simply owns other managed objects that already implement IDisposable, your class will normally dispose those objects and let their implementations handle whatever lies underneath.
For example, if your own class contains a FileStream, it usually does not need its own finalizer merely because it owns that stream.
public void Dispose()
{
_stream.Dispose();
}The FileStream already knows how to handle its underlying resources.
Finalizers become more relevant when a class directly owns unmanaged resources and needs a last-resort mechanism in case deterministic cleanup did not happen.
A finalizer is therefore a fallback, not the normal cleanup mechanism.
The Garbage Collector may eventually process the object, but Dispose() gives your program a predictable moment to release the resource when you know you are finished with it. Useful as a last resort. Not a good cleanup policy.
The Rule to Keep
The Garbage Collector reclaims memory when objects are no longer reachable. Dispose() releases resources when your program knows it is finished with them.
Add that distinction to your coding routine.
When you create or receive an object, ask whether it only contains ordinary managed data or whether it is holding something that must be closed, returned, stopped, or released.
If the type implements IDisposable, check who owns it and where its lifetime should end.
If you own it, prefer using so cleanup is tied to the scope in which the resource is actually needed.
Remember the division of responsibilities.
Garbage Collector
↓
Reclaims managed memoryIDisposable
↓
Defines a cleanup contractDispose()
↓
Contains the cleanup behavior
written by the typeusing
↓
Makes sure Dispose is called
at the end of the scope
Like this article? For more deep dives into creative obsessions, literary inspirations, game development stories, philosophical rabbit holes, and other hyperfixations turned into writing, visit www.onlyfangirling.com.







