C# / Basic: Typed Programs and Object Contracts / Lesson 6
Exceptions and Deterministic Resource Cleanup
Distinguish expected invalid input from exceptional failure and keep resource release correct on every exit path.
On this page
Lesson goals
- Choose Try-style results or exceptions from the contract.
- Preserve exception context and catch at useful boundaries.
- Use using and finally without confusing disposal with garbage collection.
Failures belong in the operation’s contract
An exception signals that an operation could not complete under its contract. Routine invalid user text often fits TryParse better than throwing and catching on every attempt. A violated required argument can justify ArgumentException or a more specific subtype. Choose a consistent model so callers know which failures they are expected to handle.
Do not use a magic numeric value when that value is also valid data. A Try-style Boolean plus output, a domain result type, or a documented exception can each express different needs. Keep programmer invariant failures distinct from ordinary absence and resource failures; one broad catch does not make all of them recoverable.
Catch a specific demonstrated failure
The first program deliberately performs a checked addition outside int range and catches only OverflowException. The output is a stable application message, not the runtime’s localized exception wording. This is an educational controlled failure; a normal bounded addition API might instead precheck and return a Try result.
A catch should have a real recovery, translation, or reporting purpose. Catching Exception and continuing with partly updated state can corrupt later behavior. If a layer cannot add useful context or restore its invariant, allow the failure to reach an owning boundary rather than logging and swallowing it repeatedly.
using System;
int maximum = int.MaxValue;
try
{
int result = checked(maximum + 1);
Console.WriteLine(result);
}
catch (OverflowException)
{
Console.WriteLine("addition rejected");
}
addition rejected
The checked addition throws before a result is used, and the specific catch prints a stable message.
Preserve the original diagnostic path
Inside a catch, throw; rethrows while preserving the original stack information. Throwing the caught variable with throw ex; changes the rethrow stack context and can hide where the failure began. When translating an exception into a public category, retain the inner exception where appropriate and avoid leaking secrets through a message.
Use exception filters when they clarify which failures belong to a handler. Do not catch cancellation indiscriminately as an ordinary failure; later lessons identify the relevant token and operation. Finally runs as control leaves its protected region in ordinary managed unwinding, making it suitable for cleanup that must happen on success and exception paths.
Dispose a resource when its logical scope ends
The second example owns a MemoryStream only within a using statement. Its length is observed while the stream is live, then disposal occurs before the final message. No file is opened or removed. The point is deterministic resource cleanup, which also applies to files and handles whose lifetime should not wait for garbage collection.
using expresses disposal responsibility, not object copying or a promise that all memory disappears immediately. A helper receiving a borrowed Stream should not normally dispose it unless the API explicitly transfers ownership. Some wrappers offer leaveOpen options; choose those from the actual ownership relationship rather than guessing.
using System;
using System.IO;
using (var stream = new MemoryStream())
{
stream.WriteByte(7);
Console.WriteLine($"length={stream.Length}");
}
Console.WriteLine("scope complete");
length=1
scope complete
The byte is written while the stream is live; leaving using disposes it before the final line.
Design ownership before adding catch blocks
List acquired resources and define one owner for each. On partial construction failure, release only what was acquired, usually in reverse order. Cleanup itself can fail, so decide which error should be preserved and avoid replacing a useful original failure accidentally. IDisposable does not mean every method becomes thread-safe or every duplicate Dispose call is part of its documented contract.
Test success, acquisition failure, operation failure, and early return. Verify the resulting state as well as the exception type. A program that merely avoids crashing after swallowing all exceptions may have lost data or leaked resources. Reusable code should return useful failures and let the application choose presentation and exit status.
Common errors and fixes
A catch prints an error and leaves invalid state in use.
- Why it happens
- Recovery was assumed without restoring the invariant.
- How to fix it
- Restore a valid state or propagate a documented failure.
- Prevent it next time
- Test post-failure state explicitly.
A rethrow obscures the original failure location.
- Why it happens
- throw ex was used instead of an unqualified rethrow.
- How to fix it
- Use throw; when preserving the caught exception.
- Prevent it next time
- Review translation boundaries and inner exceptions.
A borrowed stream is unexpectedly closed.
- Why it happens
- Disposal responsibility was guessed.
- How to fix it
- Leave borrowed resources open unless transfer is explicit.
- Prevent it next time
- Document creation, ownership, and leave-open behavior.
Check your understanding
1. A function opens two resources and acquisition of the second fails. Describe the valid cleanup and reporting behavior.
Show worked solution
The first resource remains owned and must be disposed once; no second resource exists to release. Preserve the acquisition failure and ensure cleanup does not silently replace it. Nested using scopes or a carefully structured owner can express the reverse cleanup order. Do not publish a partially initialized result as if construction succeeded.
2. Why is garbage collection insufficient for prompt file-handle release even when it eventually reclaims an unreachable wrapper?
Show worked solution
GC timing follows memory-management needs rather than the application’s file ownership deadline. Handles are finite external resources, and finalization is neither a prompt cleanup protocol nor a substitute for Dispose. Use deterministic ownership with using or finally and keep the resource alive only as long as required. Test cleanup on exception and early-return paths.
Key takeaways
- Catch only where a meaningful recovery or translation exists.
- Disposal is deterministic ownership; garbage collection is memory reclamation.
- Failure tests must inspect state and cleanup, not only exception survival.