A failure needs a decision
An exception communicates that an operation did not finish through its normal path. Its type, context and the caller’s contract determine the response. For an optional file, absence may permit defaults; insufficient permission does not prove absence and invalid content should not be silently accepted.
Limit try to the operation whose failure you know how to interpret. Wrapping reading, parsing and activation in one generic catch can misclassify a later programming error as a missing file. A specific catch can convert an expected failure into an explicit result; if you only add diagnostic information, use throw; to continue propagation while retaining the stack trace.
Message text is useful for diagnosis but does not replace classification by type and context. Avoid turning every failure into an apparently valid result.
Handlers, filters and scope exit
Catch clauses are considered in written order; a when filter adds a condition to the type. If optional is false, catch (IOException) when (optional) does not handle the exception and the search continues. A later handler can handle the same type without that filter.
A finally performs exit actions during normal flow or ordinary exception unwinding, including return; it does not mean the operation was accepted. Keep cleanup code small: if finally itself throws another exception, that failure can replace the one being propagated.
Do not treat a cleanup call as proof of rollback either: disposing a reader or connection does not automatically undo business effects already produced. If a process terminates abruptly, do not build durable recovery guarantees solely on finally.
using defines who disposes and when
The garbage collector manages object memory; resources with Dispose may need release before collection. A using block limits resource use to its scope. A using var reader = ...; declaration disposes it at the end of the scope containing the declaration, not at the end of that line.
An early return still passes through disposal when execution follows ordinary exit mechanisms. Two nested acquisitions are disposed from inside outward. If OpenA succeeds and OpenB throws before returning a resource, the outer using still disposes A; no B was delivered to the using for disposal.
The OpenB implementation is responsible for resources it acquired internally before failing. Do not confuse the readonly reference declared by using with an immutable object.
Owned resources and supplied resources
A function that opens a file and completes a read normally owns closure of the reader it created. When it receives a Stream from its caller, ownership must be agreed. By default, disposing a StreamReader also closes the stream supplied to it.
The overload with leaveOpen: true permits reader disposal while keeping that stream open for its owner. This does not promise to retain the initial position: reading and buffering can advance the stream, and the caller must manage position according to its protocol.
Simply removing using to avoid closing a borrowed resource is not sufficient either; it leaves reader disposal unfulfilled. When designing an API, state whether the resource is transferred, borrowed or created internally. That decision should hold on both success and failure.
Files: observing does not reserve the resource
File.ReadAllText opens, reads and closes the file, returning a string. It does not transfer an open handle, and accessing the string does not reread disk. For very large content, obtaining all text at once can exceed the memory budget; choose a strategy compatible with input limits.
File.Exists is only an observation and can return false for causes other than absence, such as some access errors. Even after true, another process can remove or replace the file before the read. Handle the actual operation’s outcome.
Also define the base for relative paths. On Linux, Path.Combine("/srv/imports", "/tmp/item.txt") returns the second absolute path. Combining strings does not check that a destination remains inside an authorized directory or replace the application’s access rules.
Do not return work dependent on a closed resource
Consider an ordinary method that declares using var reader = new StringReader("A\nB") and returns new[] { 1, 2 }.Select(_ => reader.ReadLine()). The result is a deferred query. When the method finishes, using disposes reader; only afterward does the consumer attempt enumeration, and the first read encounters a closed resource.
Retaining the captured reference does not reverse Dispose. For a small input, you can complete the reads and materialize the values before leaving the scope. Another API might explicitly transfer the resource, but it needs an owner and closure rules.
Do not confuse this ordinary method with an iterator method using yield: that design has a different lifetime and must be analyzed as such. When reviewing a function, ask whether the return value already contains independent data or still requires later I/O.
Guided reading: accept a title before returning
The example opens an application-controlled text file and obtains its first line before returning. Assume a trusted path, valid UTF-8 text and line lengths bounded by the process producing the file. The method treats an empty file or an empty or whitespace-only first line as invalid content.
A title " Daily status " becomes "Daily status". The return value is an already obtained string, so closing reader does not prevent the consumer from using it. Access or opening failures propagate; this method does not apply defaults.
The caller can handle an authorized absence in an appropriate scope. Reading one line does not validate every line of the file or demonstrate consistency against concurrent writers. In the questions, identify the resource owner, failure point and permitted decision without needing to execute the snippet.
using System;
using System.IO;
public static class Titles
{
public static string ReadTitle(string path)
{
using var reader = File.OpenText(path);
string? line = reader.ReadLine();
if (line is null || line.Trim().Length == 0)
throw new FormatException("A title is required.");
return line.Trim();
}
}For a controlled UTF-8 file whose first line is " Daily status ", ReadTitle returns "Daily status" and closes the reader before the caller receives the result.
Common pitfalls
Converting every exception to defaults; using throw ex; confusing using with rollback; closing borrowed streams; treating Exists as a reservation; returning queries that use already closed readers.
Related topics: LINQ, deferred work and materialization · References, nullability and text · Async tasks and cancellation
Handle only failures you know how to interpret and keep resource lifetime compatible with all work that depends on it.
Reference: Exception-handling statements · C# 14 / .NET 10; no preview features