C# Exceptions and IDisposable: try, catch, finally and using
Codevity Admin · 10/25/2026
Step 20 of 31 · C# Interview Preparation for Beginners · Stage 5: Exceptions, memory and async
Codevity Admin · 10/25/2026
Exception handling and resource cleanup appear in nearly every C# interview. The key ideas are small, but the details matter.
Both re-throw an exception you caught, but they treat the stack trace differently:
If you want to add context, wrap the original: .
An empty hides real failures and makes bugs very hard to find. At minimum, log the exception. If you cannot handle it, let it propagate.
Derive from (not ), end the name with , and add properties for the data a caller needs.
The garbage collector frees memory, but it does not promptly release unmanaged resources: file handles, database connections, sockets, native memory. A type that owns such resources implements , and you call when you are done. The statement does that for you, even if an exception occurs:
The output is , , , . A statement is compiled into a / that calls . For asynchronous cleanup, use with .
For a class that only owns other disposable objects, simply disposes them. The full pattern is needed only when the class directly owns unmanaged resources:
Most application code never needs a finalizer. Use for native handles instead, and keep it simple.
Exceptioncatchfinallycatch (IOException ex) when (ex.HResult == 5)int.TryParseFormatExceptionthrow;throw ex;throw exthrow new DataException("Could not load orders", ex);catch { }ExceptionApplicationExceptionExceptionIDisposableDispose()usingworkingclosed Adoneclosed BusingtryfinallyDispose()IAsyncDisposableawait usingDispose()SafeHandleint.TryParsetrycatchthrow ex;ex.StackTracetry
{
int result = int.Parse("abc");
Console.WriteLine(result);
}
catch (FormatException ex)
{
Console.WriteLine($"Bad number: {ex.Message}");
}
catch (Exception ex) // the general handler goes last
{
Console.WriteLine($"Unexpected: {ex.Message}");
}
finally // cleanup: runs whether or not an exception was thrown
{
Console.WriteLine("always runs");
}try { WithRethrow(); }
catch (Exception ex) { Console.WriteLine(ex.StackTrace!.Contains("Thrower")); } // True
try { WithThrowEx(); }
catch (Exception ex) { Console.WriteLine(ex.StackTrace!.Contains("Thrower")); } // False
static void Thrower() => throw new InvalidOperationException("boom");
static void WithRethrow() { try { Thrower(); } catch { throw; } }
static void WithThrowEx() { try { Thrower(); } catch (Exception ex) { throw ex; } }try
{
throw new InsufficientFundsException(30m);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"{ex.Message} (short by {ex.Shortfall})"); // Not enough money. (short by 30)
}
public class InsufficientFundsException : Exception
{
public decimal Shortfall { get; }
public InsufficientFundsException(decimal shortfall) : base("Not enough money.")
{
Shortfall = shortfall;
}
}using (var connection = new FakeConnection("A"))
{
Console.WriteLine("working");
} // Dispose() runs here, then the output continues
using var second = new FakeConnection("B"); // using declaration: disposed at the end of the enclosing scope
Console.WriteLine("done");
public sealed class FakeConnection : IDisposable
{
private readonly string _name;
public FakeConnection(string name) => _name = name;
public void Dispose() => Console.WriteLine($"closed {_name}");
}public class NativeResourceOwner : IDisposable
{
private bool _disposed;
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this); // no need to run the finalizer now
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// dispose managed objects here
}
// release unmanaged resources here
_disposed = true;
}
~NativeResourceOwner() => Dispose(disposing: false); // safety net if Dispose was never called
}