1. Introduction
In programming, file safety means not only protection from viruses and hackers, but also handling errors, locks, permissions, bad paths, and other gotchas properly. Sloppy handling can lead to data loss, app hangs, weird bugs, or the infamous IOException.
Here are classic situations we want to handle:
- The file doesn't exist, but we're trying to read it (or the opposite: it already exists and we want to create only-if-not).
- The file is open in another program and locked.
- The user doesn't have read or write permissions for that file or folder.
- The file path is incorrect or contains forbidden characters.
- The operation is unexpectedly interrupted (e.g., disk runs out of space).
- Bad practice: leaving files open, not closing streams.
Luckily, .NET gives you all the tools to solve these problems. They're simple — but like seat belts, the main thing is to remember to use them.
2. Basic principles for safe file handling
Check file existence and permissions ahead of time
Before reading a file, check that it exists (for example with File.Exists), especially if the path comes from the user:
string path = "test.txt";
if (!File.Exists(path))
{
Console.WriteLine("Error: file not found.");
return;
}
Before writing, make sure the directory exists and you have write permissions (or let the error bubble up).
Never leave streams open
Use the using keyword to automatically close streams — that's best practice:
using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello, files!");
// The file is closed here, even if an error happened!
This protects against stuck locks and resource leaks.
Always catch exceptions
Any file operation can throw. Even if the file was there a moment ago — it can be deleted/moved while you access it. Use try-catch:
try
{
using var reader = new StreamReader("data.txt");
string line = reader.ReadLine();
Console.WriteLine(line);
}
catch (FileNotFoundException)
{
Console.WriteLine("File not found.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("No access to the file.");
}
catch (IOException ex)
{
Console.WriteLine($"I/O error: {ex.Message}");
}
Don't trust user input
If the file path is provided by the user, they may type it wrong (or try to crash your app). Validate the path (see Path.GetInvalidPathChars()):
try
{
string userPath = Console.ReadLine()!;
if (string.IsNullOrWhiteSpace(userPath))
{
Console.WriteLine("Path can't be empty!");
return;
}
// Extra: check for invalid chars
foreach (char c in Path.GetInvalidPathChars())
if (userPath.Contains(c))
{
Console.WriteLine("Path contains invalid characters.");
return;
}
// Now it's safer to work with the file
}
catch (Exception ex)
{
Console.WriteLine("Path validation error: " + ex.Message);
}
3. Practical example: reading a file "like a pro"
Let's level up our demo project. Imagine we need to read a file whose name the user types and print its contents. All with error handling and proper encoding.
Console.Write("Enter the file path: ");
string? path = Console.ReadLine();
// Path validation
if (string.IsNullOrWhiteSpace(path))
{
Console.WriteLine("Path can't be empty!");
return;
}
foreach (char c in Path.GetInvalidPathChars())
if (path.Contains(c))
{
Console.WriteLine("Path contains invalid characters.");
return;
}
// Try reading the file
try
{
if (!File.Exists(path))
{
Console.WriteLine("File not found.");
return;
}
// Specify encoding explicitly (e.g., UTF-8)
using var reader = new StreamReader(path, Encoding.UTF8);
string content = reader.ReadToEnd();
Console.WriteLine("File contents:");
Console.WriteLine(content);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("No permissions to read the file.");
}
catch (IOException ex)
{
Console.WriteLine("I/O error: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Unexpected error: " + ex.Message);
}
Note that this code won't crash if the file disappears between typing the name and reading: the exception will be caught. The stream is closed automatically after the using block — even on error.
4. Files for writing: avoid data loss
When opening a file for writing, especially in overwrite mode (false in the second parameter of the StreamWriter constructor), you risk accidentally wiping important data. Here are some tips:
Check whether you're overwriting an existing file
Sometimes it's useful to ask the user if the file already exists:
if (File.Exists(path))
{
Console.WriteLine("Warning: file already exists. Overwrite? (y/n)");
string answer = Console.ReadLine()!;
if (!answer.Equals("y", StringComparison.OrdinalIgnoreCase))
return;
}
Use append mode where appropriate
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine(DateTime.Now + ": new log entry.");
Old information won't disappear.
5. Protect against races and conflicts
Sometimes multiple programs can open the same file concurrently (for example your C# client and Notepad++). That can cause errors. By default StreamReader and StreamWriter use sharing modes based on FileShare — meaning they either allow others to read or deny access.
You can control this explicitly:
using var stream = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
// Reading...
- FileShare.Read: others can only read.
- FileShare.None: no sharing — the file is exclusively yours.
If you need a file to be readable and writable by different programs at the same time — use the appropriate mode, but be careful: this is usually done only when you understand the consequences.
6. Unexpected exceptions and what to do
Even if you follow all advice strictly, failures can still happen — e.g., out of disk space, a broken flash drive, or malware. Here are some "exotic" exceptions and how to catch them:
- PathTooLongException — the file path is too long (over 260 chars on old Windows).
- DirectoryNotFoundException — the specified directory was not found.
- DriveNotFoundException — for example if the path "Z:\\file.txt" and drive Z doesn't exist.
- NotSupportedException — for example the path contains an unsupported combination.
It's recommended to have separate handling or at least log these exceptions separately.
7. Use temporary files for atomic writes
Classic problem: you write a file but the program crashes halfway — you end up with a corrupted file. Professional apps often use an "atomic" write strategy:
- Write content to a temporary file (e.g., "file.txt.tmp").
- Move the temporary file (the operation is usually atomic at the filesystem level) over the target (File.Replace).
- The old file is either fully replaced or left unchanged — no half-broken data.
Example:
string tempPath = path + ".tmp";
try
{
using var writer = new StreamWriter(tempPath, false, Encoding.UTF8);
// Write everything to the temp file
writer.Write(contentForSave);
// After successful write, replace the main file
File.Replace(tempPath, path, null); // Moves the temp file, replacing the target (atomic operation)
}
catch (Exception ex)
{
Console.WriteLine("Error saving file: " + ex.Message);
// It's a good idea to delete tempPath if it's not needed
}
Many editors and office apps do this in real life to guarantee data consistency.
8. Use wrapper helpers for safe access
.NET has helper methods for "safe" file ops. For example, File.ReadAllText and File.WriteAllText open, read/write and close the file for you. But even these should be wrapped with try-catch:
try
{
string text = File.ReadAllText("settings.json", Encoding.UTF8);
// Work with the data...
}
catch (Exception ex)
{
Console.WriteLine("File error: " + ex.Message);
}
For large files, use streams and read in chunks so you don't eat all the memory.
GO TO FULL VERSION