C# Dispose vs Finalizer: When to Use Each
c# dispose vs finalizer: Understand the difference between IDisposable and finalizers in C#, when each runs, and how to implement the dispose pattern correctly.
In C#, every object that holds a resource that the garbage collector does not manage must eventually release it. The two mechanisms for that are IDisposable.Dispose and a finalizer. They serve different purposes, run at different times, and are not interchangeable. Understanding c# dispose vs finalizer is the difference between deterministic cleanup and a last-resort safety net.
What the Runtime Guarantees vs What You Control
The garbage collector (GC) manages memory for managed objects. It does not know how to release unmanaged resources such as file handles, sockets, or native memory. A finalizer is a method the GC calls when it collects an object that has one. It is the runtime's way of giving an object a chance to clean up before its memory is reclaimed.
Dispose, on the other hand, is a method you call explicitly. It runs immediately, on the calling thread, and gives you full control over when the resource is released. The using statement is the idiomatic way to call Dispose automatically when a block exits.
using (var stream = File.OpenRead("data.txt")) { // work with stream } // stream.Dispose() has already been called
A finalizer cannot be called from your code directly. You cannot predict when it runs, or even if it runs before the process ends. A normal program can exit without finalizers executing.
Why Finalizers Are Not a Cleanup Mechanism
A finalizer is a safety net, not a primary cleanup path. If you rely on a finalizer to release a resource, you are deferring that release to an unknown point in the future. The GC decides when to run finalizers based on memory pressure and the state of the finalization queue.
Consider a class that holds an unmanaged handle and only implements a finalizer:
public class UnmanagedResource { private IntPtr _handle; public UnmanagedResource() { _handle = NativeMethods.CreateResource(); } ~UnmanagedResource() { NativeMethods.ReleaseResource(_handle); } }
The resource stays alive until the GC decides to collect the object. If the process creates many such objects, the finalization queue grows, and the finalizer thread may fall behind. The resources are held much longer than necessary, potentially exhausting the underlying native resource.
The IDisposable Pattern for Deterministic Cleanup
IDisposable is the interface that lets callers release resources explicitly. The typical implementation looks like this:
public class ManagedResource : IDisposable { private IntPtr _handle; private bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // release managed resources here } // release unmanaged resources here if (_handle != IntPtr.Zero) { NativeMethods.ReleaseResource(_handle); _handle = IntPtr.Zero; } _disposed = true; } ~ManagedResource() { Dispose(false); } }
This is the full dispose pattern. The parameterless Dispose calls the protected Dispose(bool) with true, then suppresses finalization. The finalizer calls it with false, indicating that managed resources should not be touched because the GC may have already collected them.
The disposing flag matters. During finalization, you cannot safely reference other managed objects because they may already be finalized. You can only release unmanaged resources directly.
When to Implement a Finalizer at All
Most classes that implement IDisposable do not need a finalizer. If your class holds only managed resources, such as a Stream or a SqlConnection, those resources themselves have finalizers if needed. Your class should simply call Dispose on them and not add its own finalizer.
A finalizer is necessary only when your class directly holds an unmanaged resource that the object itself must release. If you wrap an unmanaged resource in a SafeHandle or a SafeBuffer, the safe handle already has a finalizer. You can rely on that and avoid writing your own.
public class SafeResource : IDisposable { private readonly SafeFileHandle _handle; public SafeResource(string path) { _handle = File.OpenHandle(path); } public void Dispose() { _handle.Dispose(); } }
No finalizer is needed here because SafeFileHandle provides the fallback. Adding a finalizer would only increase finalization pressure without improving correctness.
The Cost of Finalization
Objects with finalizers are more expensive for the GC. When such an object is unreachable, the GC places it on the finalization queue instead of reclaiming its memory immediately. A separate finalizer thread runs the finalizer, and only then is the object considered truly dead and eligible for collection. That means a finalizable object survives at least one extra GC generation.
If your application creates many finalizable objects, the GC must track them, the finalizer thread must process them, and memory is not reclaimed as promptly. This can lead to higher memory usage and longer GC pauses. The GC.SuppressFinalize(this) call in Dispose is critical because it tells the GC that the object no longer needs finalization, so it can be collected normally.
Common Pitfalls in Dispose and Finalizer Implementations
One common mistake is calling Dispose from a finalizer and expecting it to release managed resources. As shown above, the disposing flag prevents that. Another is forgetting to call GC.SuppressFinalize in Dispose. Without that, the object will still be finalized even after explicit disposal, causing a second, unnecessary cleanup attempt.
Another pitfall is throwing exceptions from a finalizer. If a finalizer throws, the runtime treats it as a fatal error and the process terminates. Finalizers must be exception-safe. They should catch all exceptions and never propagate them.
A third issue is implementing IDisposable without a disposing flag when the class is not sealed. If a derived class adds its own resources, the base class must provide a protected virtual Dispose(bool) so derived classes can extend the cleanup logic. Without it, derived classes have no clean way to release their own resources during disposal.
Choosing Between Dispose and Finalizer in Practice
The decision is not really a choice between the two. Dispose is the primary mechanism for releasing resources deterministically. A finalizer is a backup that exists to prevent leaks if a caller forgets to call Dispose. You should implement IDisposable whenever your class owns a resource that should be released promptly. You should add a finalizer only when your class directly owns an unmanaged resource and you cannot use a safe handle.
For most application code, you will never write a finalizer. You will use using statements, await using for IAsyncDisposable, and call Dispose on objects that implement IDisposable. The finalizer appears mainly in framework code, low-level wrappers, and classes that interop with native libraries.
A sealed class that only wraps a managed IDisposable can be simpler:
public sealed class DatabaseConnection : IDisposable { private readonly SqlConnection _connection; public DatabaseConnection(string connectionString) { _connection = new SqlConnection(connectionString); } public void Dispose() { _connection.Dispose(); } }
No finalizer, no Dispose(bool), no _disposed flag. The SqlConnection handles its own unmanaged resources. Adding more complexity would be unnecessary.
Finalizer and Dispose Interaction with Inheritance
When a base class implements the dispose pattern, derived classes must call the base implementation. The common pattern is to override Dispose(bool) and call base.Dispose(disposing). If a derived class adds an unmanaged resource, it must also implement a finalizer that calls Dispose(false). The base class finalizer will run as well, so the derived finalizer must call the base finalizer explicitly.
public class DerivedResource : BaseResource { private IntPtr _nativeBuffer; protected override void Dispose(bool disposing) { if (disposing) { // release managed resources } if (_nativeBuffer != IntPtr.Zero) { NativeMethods.ReleaseBuffer(_nativeBuffer); _nativeBuffer = IntPtr.Zero; } base.Dispose(disposing); } ~DerivedResource() { Dispose(false); } }
If the base class does not have a finalizer, the derived class finalizer still works, but it must call Dispose(false) on itself. The base class's Dispose(bool) will run through the override chain.
Performance and Reliability in Production
In production, the most reliable way to ensure resources are released is to use using blocks or try/finally with explicit Dispose. This gives you deterministic behavior and avoids relying on the finalizer thread. For long-running services, finalization can become a bottleneck if many objects are finalized slowly. Monitoring the finalization queue length and the number of finalizable objects can help detect leaks.
If you see a growing number of finalizable objects in memory dumps, it usually means Dispose is not being called and finalizers are the only cleanup path. Fix the call sites rather than optimizing the finalizer. The finalizer should be a rare fallback, not the primary release mechanism.
A finalizer that does too much work can also delay the finalizer thread. Keep finalizers short. Release the unmanaged resource immediately and do not perform logging, database updates, or other operations that can throw or block.