Skip to main content
What is Improper Disposals? Improper Disposal refers to the failure to properly release unmanaged resources (e.g., file handles, database connections, network sockets) when they are no longer needed by an application. While the .NET garbage collector (GC) efficiently manages the release of memory for managed objects, it has no knowledge of unmanaged resources external to the .NET runtime. Improper disposal leads to resource leaks, which can cripple an application by causing instability, performance degradation, and eventual crashes when system limits are exhausted (e.g., running out of available file handles). The primary mechanism for preventing this in C# is the IDisposable pattern.

How it works in C#

IDisposable

The IDisposable interface is the cornerstone of proper resource cleanup in C#. It defines a single method, Dispose(), which a class implements to release its held unmanaged resources. The pattern dictates that once an object is disposed, it should be considered invalid and any attempt to use it should throw an ObjectDisposedException.
using System;

// A class that owns an unmanaged resource, implementing IDisposable.
public class FileLogger : IDisposable
{
    private System.IO.StreamWriter _fileStream;

    // A flag to track if the object has already been disposed.
    private bool _disposed = false;

    public FileLogger(string filePath)
    {
        // Acquire the unmanaged resource (a file handle).
        _fileStream = new System.IO.StreamWriter(filePath);
    }

    public void Log(string message)
    {
        if (_disposed)
            throw new ObjectDisposedException("FileLogger");

        _fileStream.WriteLine(message);
    }

    // The public Dispose method, called by the consumer.
    public void Dispose()
    {
        Dispose(true);
        // Suppress finalization because the resource was already cleaned up.
        GC.SuppressFinalize(this);
    }

    // The core disposal logic, reusable by the finalizer.
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Release other managed objects (if any).
                // _fileStream is a managed object that ALSO implements IDisposable.
                _fileStream?.Dispose();
            }

            // Release unmanaged resources directly (e.g., Win32 handles) here.
            // In this case, _fileStream.Dispose() handles the unmanaged file handle.
            _disposed = true;
        }
    }

    // Finalizer (destructor) - a safety net if Dispose is not called.
    ~FileLogger()
    {
        Dispose(false);
    }
}

Using statements

The using statement provides a syntactically clean and reliable way to ensure that Dispose() is called, even if an exception occurs within the block. It translates into a try/finally construct in the compiled code, guaranteeing the disposal of the resource.
// The 'using' statement ensures _logger.Dispose() is called upon exiting the block.
using (var _logger = new FileLogger("log.txt"))
{
    _logger.Log("This message is written to the file.");
    // Even if an exception is thrown here, Dispose() will be called.
} // _logger.Dispose() is automatically called here.

// Modern C# (version 8.0+) also supports a using declaration, which disposes at the end of the scope.
using var _logger = new FileLogger("log.txt");
_logger.Log("This message is written to the file.");
} // _logger is disposed here when it goes out of scope.

Finalizers

A finalizer (declared with a C# destructor syntax ~ClassName()) is a method called by the garbage collector at an indeterminate time before it reclaims the object’s memory. It acts as a safety net to release unmanaged resources only if the developer failed to call Dispose(). Relying on finalizers is problematic because they are non-deterministic, can hurt performance, and the objects they reference may have already been garbage collected.
// As shown in the FileLogger example above:
~FileLogger()
{
    // When called by the GC, 'disposing' is false. We only clean up unmanaged resources.
    // We cannot touch managed objects (like _fileStream) as they may have been finalized already.
    Dispose(false);
}

Safe handles

The SafeHandle class (and its derivatives like SafeFileHandle, SafeWaitHandle) is a critical, modern best-practice for wrapping unmanaged resources. It encapsulates the handle and implements the IDisposable and finalizer patterns robustly, eliminating the need for you to write a finalizer manually. This drastically reduces the chance of errors in critical resource management code.
using System;
using Microsoft.Win32.SafeHandles;

public class UnmanagedResourceWrapper : IDisposable
{
    // Using SafeHandle to wrap the unmanaged resource.
    private SafeFileHandle _safeHandle;

    public UnmanagedResourceWrapper(string fileName)
    {
        // P/Invoke call to open a file. Assume it returns an IntPtr.
        IntPtr fileHandle = NativeMethods.OpenFile(fileName);

        // Wrap the raw handle in a SafeHandle. This is now a managed object.
        _safeHandle = new SafeFileHandle(fileHandle, true);
    }

    public void UseResource()
    {
        // Use the SafeHandle to perform operations.
        if (_safeHandle.IsInvalid)
            throw new InvalidOperationException("Handle is invalid.");

        // ... work with the handle via _safeHandle.DangerousGetHandle() if necessary.
    }

    // Dispose implementation becomes trivial. SafeHandle owns the disposal logic.
    public void Dispose()
    {
        _safeHandle?.Dispose();
    }

    // NO NEED TO WRITE A FINALIZER. SafeHandle has its own, which will ensure the resource is closed.
}

internal static class NativeMethods
{
    // Simulated P/Invoke signature.
    [System.Runtime.InteropServices.DllImport("kernel32.dll")]
    internal static extern IntPtr OpenFile(string fileName);
}

Why is Improper Disposals important?

  1. Resource Exhaustion Prevention (Scalability Principle): Proper disposal ensures finite system resources (handles, connections) are released predictably, preventing leaks that would limit the application’s ability to scale and handle long-running tasks or high load.
  2. Deterministic Cleanup (Single Responsibility Principle): The IDisposable pattern gives a class a single, clear responsibility for managing its resources’ lifecycle, allowing developers to control exactly when cleanup occurs, rather than relying on the non-deterministic garbage collector.
  3. Predictable Application State (Robustness Principle): Ensuring resources are closed properly (e.g., files are flushed, transactions are committed/rolled back) maintains data integrity and prevents the application from entering an inconsistent or corrupted state.

Advanced Nuances

  1. When to Write a Finalizer (Hint: Almost Never): The primary rule is to avoid writing finalizers unless you directly own an unmanaged resource that is not already wrapped by a SafeHandle. If your class only uses managed objects that implement IDisposable (like FileStream), you do not need a finalizer; simply disposing of those managed objects in your Dispose(bool) method is sufficient. The finalizer is only for the raw, unmanaged resource.
  2. Inheritance and the Disposable Pattern: The canonical Dispose(bool disposing) pattern is protected and virtual for a key reason: to allow derived classes to properly override disposal behavior. A derived class can override Dispose(bool) to clean up its own resources and then call base.Dispose(disposing).
  3. IDisposable and IAsyncDisposable: Modern .NET introduces IAsyncDisposable for scenarios where resource cleanup is potentially long-running (e.g., flushing a large buffer to a cloud storage stream). This is used with the await using statement. A class can implement both interfaces if it supports synchronous and asynchronous disposal.

How this fits the Roadmap

Within the “Resource Management” section of the Advanced C# Mastery roadmap, “Improper Disposals” is a foundational pillar. A solid grasp of IDisposable, using, and SafeHandle is an absolute prerequisite for tackling more advanced topics.
  • Prerequisite For: It underpins understanding Dependency Injection (DI) Container Lifetimes (e.g., how a Scoped or Singleton service in ASP.NET Core is disposed), Object Pooling (knowing how to reset and return objects safely), and working with IAsyncDisposable.
  • Unlocks: Mastering this concept unlocks the ability to safely and efficiently work with advanced topics like Platform Invoke (P/Invoke) and custom interoperability, where you directly manage unmanaged memory and handles. It also is essential for understanding the internals of high-performance libraries (e.g., System.IO.Pipelines, Microsoft.Data.SqlClient) that heavily rely on precise resource lifetime management.

Build docs developers (and LLMs) love