Exception Handling
The library is designed to make exception handling boringly predictable: parsing throws ArgumentException, file system operations throw IOException, and nothing else needs to be accounted for. That separation lets you write tidy try/catch blocks without resorting to catch (Exception) to corral the mix of unrelated exception types System.IO would otherwise throw.
The Two Exception Families
- Parsing throws ArgumentException (and subtypes). Every input-validation error during path construction comes from this family.
- I/O throws IOException (and subtypes). Every error from a file system operation comes from this family, including permission errors, which are surfaced as UnauthorizedIOAccessException (a subclass of IOException).
Parse-Time Errors
ArgumentException (or ArgumentNullException / ArgumentOutOfRangeException for null or invalid arguments) is the only exception family thrown by parsing and path-manipulation methods. Catch it where you accept untrusted input:
IFilePath file;
try
{
file = FilePath.Parse(userInput);
}
catch (ArgumentException ex)
{
log.Warn($"Invalid path: {ex.Message}");
return;
}
Exception messages are detailed enough to act on directly. They name the exact rule that failed (e.g. "Entry name ends with a dot.", "Attempt to navigate past root directory.").
Parsing Without Exceptions
When invalid input is a routine outcome and you don't need the reason, skip the exception entirely with the Try parse methods: FilePath.TryParse, FilePath.TryParseAbsolute, FilePath.TryParseRelative and their DirectoryPath equivalents. They return false instead of throwing:
if (!FilePath.TryParse(userInput, out IFilePath? file))
{
log.Warn("Invalid path.");
return;
}
Tip
The Try methods don't say why the input was rejected. If you want to show the user the specific rule that failed, parse with the throwing method and report the exception message as in the first example. See Parsing Paths.
I/O-Time Errors
Every method that touches the file system throws only IOException and its subtypes, across both Windows and Unix. The full set you can encounter:
| Exception | Typical cause |
|---|---|
| FileNotFoundException | A file expected to exist is missing. |
| DirectoryNotFoundException | A directory expected to exist is missing. |
| PathTooLongException | The fully-qualified path exceeds the OS limit. |
| UnauthorizedIOAccessException | The OS denied access. |
| IOException | Anything else (file in use, disk full, network failure, etc.). |
A single base-type catch handles all of them:
try
{
using FileStream s = file.OpenStream(FileMode.Open, FileAccess.Read, FileShare.Read);
return Read(s);
}
catch (IOException ex)
{
log.Warn($"Could not read {file.PathDisplay}: {ex.Message}");
return null;
}
UnauthorizedIOAccessException
System.IO surfaces permission errors as UnauthorizedAccessException, which does not derive from IOException. That forces you to catch two unrelated base types to handle "any I/O failure".
This library raises UnauthorizedIOAccessException instead (a direct IOException subclass), so a single catch (IOException) covers every failure mode. If you specifically need to distinguish access denial:
try
{
file.Delete();
}
catch (UnauthorizedIOAccessException) { /* show "permission denied" UI */ }
catch (IOException) { /* something else went wrong */ }
Note
The library converts UnauthorizedAccessException thrown by underlying System.IO calls into UnauthorizedIOAccessException. You should never need to catch UnauthorizedAccessException from this library's surface.
Cross-Platform Consistency
System.IO throws different exception types on Windows and Unix for the same operation. For example, File.Delete on a directory throws UnauthorizedAccessException on Windows but IOException on Unix. This library normalizes those: the exception type for a given failure is the same across platforms.
Important
If you're catching specific exception types in code that needs to behave the same on Windows and Unix, use this library's surface; System.IO's exception types are not portable.
Search-Time Errors
Enumeration is lazy. Exceptions can be thrown during the iteration of a foreach, not just from the call that produced the enumerable. Catch around the iteration site:
try
{
foreach (var f in dir.GetChildFiles("*", new SearchOptions { Recursive = true }))
Process(f);
}
catch (UnauthorizedIOAccessException ex)
{
log.Warn($"Search hit a forbidden directory: {ex.Message}");
}
The behavior on inaccessible directories is configurable. See Searching and Enumeration and InaccessibleSearchBehavior.
Putting It Together
A typical input-handling pattern that keeps parsing and I/O concerns clean:
// Phase 1: validate input. Only ArgumentException is possible here.
IAbsoluteFilePath file;
try
{
file = FilePath.ParseAbsolute(userInput);
}
catch (ArgumentException ex)
{
return Result.Invalid(ex.Message);
}
// Phase 2: do I/O. Only IOException family is possible here.
try
{
using FileStream s = file.OpenStream(FileMode.Open, FileAccess.Read, FileShare.Read);
return Result.Ok(Read(s));
}
catch (FileNotFoundException) { return Result.NotFound(); }
catch (UnauthorizedIOAccessException ex) { return Result.Forbidden(ex.Message); }
catch (IOException ex) { return Result.Error(ex.Message); }
Next Steps
The operations behind these exceptions are covered in:
- Parsing Paths: the throwing and non-throwing parse methods.
- Searching and Enumeration: search-time error handling and InaccessibleSearchBehavior.
- Working with Files and Working with Directories: the operations covered by the I/O-side catches.