← Open in the full interactive course (progress tracking, search & more)

The moment your program touches the disk, it's touching something it doesn't fully control — and that changes how you have to write code.

Everything you've written so far has lived entirely in memory — variables, objects, collections — all of it gone the instant the program ends. Real applications need to outlive that: a log service that writes lines to a file, a report generator that saves a CSV, an import job that reads orders from a file another system dropped on disk.

The moment you read or write a file, you're no longer just talking to your own program's memory — you're talking to the operating system, a physical (or virtual) disk, a filesystem with its own rules about permissions, existence, and availability. Any of that can go wrong in ways that pure in-memory code never has to worry about: the file might not exist, another process might have it locked, the disk might be full, you might lack permission to write there.

In this lesson, you'll learn how to read and write files and directories using the File and Directory classes, and why file I/O code needs to actively plan for failure in a way that in-memory code doesn't.

What Is It?

The Simple Explanation

.NET gives you two static helper classes, right in the base class library, for working with the filesystem: System.IO.File for individual files (reading, writing, checking existence, deleting), and System.IO.Directory for folders (creating, listing contents, deleting). They're the simplest, most direct way to do everyday file work — no manual setup required.

The Technical Definition

File and Directory are static classes in the System.IO namespace that wrap lower-level operating-system file APIs. Each method is a self-contained operation — open, read/write everything, close — that internally manages the underlying resources for you. For finer control over reading and writing incrementally, you drop down to streams (the subject of the next lesson) — but for a huge share of everyday file tasks, File and Directory are all you need.

MethodWhat it does
File.Exists(path)Returns true/false — does a file exist at this path?
File.ReadAllText(path)Reads the entire file into one string.
File.ReadAllTextAsync(path)Same, asynchronously — preferred for I/O-bound work.
File.ReadAllLines(path)Reads the file into a string[], one element per line.
File.WriteAllText(path, text)Writes a string to a file, overwriting it if it already exists.
File.AppendAllText(path, text)Adds text to the end of a file without erasing what's already there.
File.Copy(src, dest)Copies a file.
File.Delete(path)Deletes a file (does nothing if it doesn't exist).
Directory.Exists(path)Returns true/false — does a directory exist?
Directory.CreateDirectory(path)Creates a directory, including any missing parent folders.
Directory.GetFiles(path)Lists the files in a directory.
Directory.Delete(path, recursive)Deletes a directory, optionally including everything inside it.

Why Does It Exist?

The Problem — Programs Need to Outlive Themselves

Variables and objects vanish the instant a process ends. But an order-import job needs to read data that arrived before it started running. A logging service needs its output to survive after the program that wrote it has stopped. A report generator needs to hand a finished file to someone else entirely. None of this is possible with in-memory state alone.

The Need

Applications need a standard, reliable way to persist data to durable storage, and to read data that other processes (or earlier runs of the same process) have already written there — without every developer having to hand-roll calls to raw operating system file APIs.

The Solution — File and Directory

System.IO.File and System.IO.Directory give you simple, high-level, cross-platform methods that hide the OS-specific complexity of opening handles, managing buffers, and closing resources correctly — for the common case of "just read this whole file" or "just write this whole file."

Big Picture

File I/O sits at the boundary between your program and the outside world — and that boundary is exactly where things can go wrong that are entirely outside your program's control:

YOUR PROGRAM ↔ THE FILESYSTEM
Your Code
Fully under your control
Filesystem / Disk
Shared, external, unpredictable
Other processes can lock, move, or delete files while you're not looking.
Permissions, disk space, and even hardware failures live entirely outside your program's control.

This is exactly why file operations pair so naturally with what you just learned about exceptions — File.ReadAllText throws FileNotFoundException if the file is missing, UnauthorizedAccessException if you lack permission, and IOException if the file is locked by another process. None of these are bugs in your code — they're the filesystem telling you something you couldn't have known in advance.

How It Works

READING A FILE SAFELY — STEP BY STEP
Step 1 — Check whether it exists (when appropriate)
if (!File.Exists(path))
{
    Console.WriteLine("No such file — nothing to import today.");
    return;
}
Step 2 — Attempt the read inside a try block
try
{
    string contents = await File.ReadAllTextAsync(path);
    ProcessContents(contents);
}
Step 3 — Catch the specific failures you can meaningfully react to
catch (FileNotFoundException)
{
    Console.WriteLine("File disappeared before we could read it.");
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("No permission to read this file.");
}
catch (IOException ex)
{
    Console.WriteLine($"The file is in use or the disk had a problem: {ex.Message}");
}

Why async matters here

File I/O involves waiting on the disk (or, for a network share, waiting on the network too) — time your program's thread doesn't need to spend blocked and idle. File.ReadAllTextAsync and File.WriteAllTextAsync let the thread go do other work while the operating system handles the actual disk activity, which is why they're the preferred choice in modern .NET code — especially in server applications handling many requests at once. (The full mechanics of async/await are their own topic later in the course; for now, just know that reaching for the ...Async version of a file method is the idiomatic default.)

Simple Example

Writing a note to disk, then reading it back:

string path = "notes.txt"; // ─── Write ─── await File.WriteAllTextAsync(path, "Remember to review the Q3 budget."); // ─── Check and read ─── if (File.Exists(path)) { string contents = await File.ReadAllTextAsync(path); Console.WriteLine(contents); // Remember to review the Q3 budget. } // ─── Append without overwriting ─── await File.AppendAllTextAsync(path, "\nAlso: follow up with the vendor."); string updated = await File.ReadAllTextAsync(path); Console.WriteLine(updated); // Remember to review the Q3 budget. // Also: follow up with the vendor. // ─── Clean up ─── File.Delete(path); Console.WriteLine(File.Exists(path)); // False

Each call — WriteAllTextAsync, ReadAllTextAsync, AppendAllTextAsync, Delete — is a complete operation in itself: it opens the file, does the work, and closes it again, all in one call. You never have to remember to manually close anything with these particular methods.

Working with directories

string reportsFolder = "reports"; // Creates the directory if it doesn't already exist (and any missing parents) Directory.CreateDirectory(reportsFolder); // List every .csv file currently in that folder string[] csvFiles = Directory.GetFiles(reportsFolder, "*.csv"); foreach (var file in csvFiles) Console.WriteLine(Path.GetFileName(file)); // Remove the whole folder, including anything left inside it if (Directory.Exists(reportsFolder)) Directory.Delete(reportsFolder, recursive: true);

Real-World Example

A background job that imports orders from CSV files dropped into an "incoming" folder, moving each one to "processed" or "failed" once handled — a realistic combination of directory listing, file reading, custom exceptions, and cleanup:

public class OrderImportJob { private readonly string _incomingFolder = "orders/incoming"; private readonly string _processedFolder = "orders/processed"; private readonly string _failedFolder = "orders/failed"; public async Task RunAsync() { Directory.CreateDirectory(_processedFolder); Directory.CreateDirectory(_failedFolder); if (!Directory.Exists(_incomingFolder)) { Console.WriteLine("No incoming folder yet — nothing to do."); return; } foreach (var filePath in Directory.GetFiles(_incomingFolder, "*.csv")) { string fileName = Path.GetFileName(filePath); try { string content = await File.ReadAllTextAsync(filePath); var orders = ParseOrders(content); foreach (var order in orders) SaveToDatabase(order); File.Move(filePath, Path.Combine(_processedFolder, fileName)); Console.WriteLine($"Imported {fileName} ({orders.Count} orders)."); } catch (Exception ex) { // Don't let one bad file stop the whole batch — quarantine it and move on File.Move(filePath, Path.Combine(_failedFolder, fileName), overwrite: true); Console.WriteLine($"Failed to import {fileName}: {ex.Message}"); } } } }

Notice this mirrors the per-item error containment pattern from the previous lessons — one malformed CSV file gets quarantined into the "failed" folder instead of halting the entire batch. That's the combination this whole module is building toward: exceptions to signal what went wrong, file operations to actually do the work, and disciplined error handling to keep one bad input from taking down everything else.

Analogy

The Shared Filing Cabinet

In-memory variables are like notes on your own private desk — nobody else can touch them, and they're always exactly as you left them. A file on disk is more like a shared filing cabinet in a busy office: other people (other processes) can open a drawer at the same time you do, someone might have locked a drawer, a folder might have been moved or thrown out entirely since you last checked, and the cabinet itself might occasionally break down.

That's exactly why file code needs defensive handling that pure in-memory code doesn't — you're not the only one with access, and the state of things can change between the moment you check and the moment you act.

Common Confusion

1. "If I check File.Exists first, I don't need a try/catch"

This is one of the most common file-handling mistakes. There's a real gap in time between the Exists check and the actual read — another process can delete the file, or a permissions change can take effect, in that exact window. This is sometimes called a "time-of-check to time-of-use" (TOCTOU) gap. Exists is useful for expected cases ("no orders today is normal"), but it's never a substitute for exception handling around the actual operation.

2. File and Directory vs. Streams

File.ReadAllText reads the entire file into memory in one call — perfect for typical config files, small reports, and CSV imports. For very large files, or when you need to process data as it arrives rather than waiting for the whole file, you use a Stream instead (the next lesson) to read or write incrementally without loading everything into memory at once.

Common Mistakes

Mistake 1 — Assuming file operations always succeed

Wrong:

string content = File.ReadAllText(userSuppliedPath); // no error handling at all ProcessContents(content);

Correct: wrap it, and catch the specific failures you expect.

try { string content = await File.ReadAllTextAsync(userSuppliedPath); ProcessContents(content); } catch (FileNotFoundException) { Console.WriteLine("That file doesn't exist."); } catch (UnauthorizedAccessException) { Console.WriteLine("You don't have permission to read that file."); }

Mistake 2 — Overwriting a file you meant to append to

Calling File.WriteAllText repeatedly in a logging loop — each call erases everything written before it. Use File.AppendAllText (or, for high-frequency writes, a StreamWriter kept open across the whole session, covered in the next lesson) when you actually want to add to a file rather than replace it.

Mistake 3 — Catching Exception everywhere instead of specific failures

A blanket catch (Exception ex) around a file read hides whether the problem was a missing file, a permissions issue, or a locked file — three very different problems that usually call for three different responses. Catch the specific types you know how to react to.

When Should I Use It?

Whole-file read/write
Config files, small reports, CSV imports — File.ReadAllText/WriteAllText are the right tool.
Listing or organizing files
Directory.GetFiles, CreateDirectory, and Delete handle folder-level bookkeeping.
Large or incremental data
Multi-gigabyte files, or writing as data arrives — reach for Streams instead (next lesson).
Structured, queryable data
Anything you'll need to search, filter, or relate — a database is usually a better fit than raw files.

Mental Model

File = simple, whole-operation helpers for a single file (read it all, write it all, delete it).
Directory = the same idea, one level up, for folders.
The filesystem = shared, external territory your program doesn't fully control.

Remember:
· Checking Exists handles the expected case; a try/catch handles everything you couldn't predict.
· Prefer the ...Async methods for I/O-bound file work.
· Catch specific exception types (FileNotFoundException, UnauthorizedAccessException, IOException) so you react appropriately to each.

Key Takeaway


Check Your Understanding

You've seen how File and Directory work, and why file operations need real error handling. Let's make sure it's clear.

1. Why does reading a file typically require more defensive error handling than reading from an in-memory List<T>?

Show answer

Correct: B

Why B is correct: Unlike an in-memory collection that only your code can touch, a file lives on shared, external storage. Other processes can delete, lock, or move it; permissions can change; the disk itself can run out of space. None of that is under your program's control, so file code has to anticipate failure in ways in-memory code doesn't need to.

Why A is incorrect: File.ReadAllText and its Async counterpart are current, idiomatic .NET APIs — not deprecated.

Why C is incorrect: In-memory collections can throw too (e.g., an out-of-range index) — the point isn't that files are the only source of exceptions, but that file operations face an entire category of external, unpredictable failure that in-memory code doesn't.

Why D is incorrect: Speed isn't the reason for the extra error handling — reliability in the face of external, uncontrolled state is.

Reinforcement: File I/O crosses a boundary into shared, external state — that's exactly why it needs deliberate exception handling.

2. You write: if (File.Exists(path)) { var text = File.ReadAllText(path); } with no try/catch. What's the risk in this code?

Show answer

Correct: B

Why B is correct: This is the "time-of-check to time-of-use" gap discussed in the lesson — a real amount of time passes between the check and the actual read, and the file's state on disk can change in that window because other processes have access too. Without a try/catch, an exception here would go completely unhandled.

Why A is incorrect: File.Exists correctly returns true when the file is present — nothing about this code prevents the block from running under normal conditions.

Why C is incorrect: This code compiles and runs fine under normal conditions — the issue is a runtime reliability gap, not a compile error.

Why D is incorrect: File.Exists is a read-only check — it never creates, modifies, or deletes anything.

Reinforcement: Exists handles the expected case, but only a try/catch around the actual operation protects against everything that can change in between.

3. Which method call would erase the existing contents of "log.txt" if it already has content, rather than adding to it?

Show answer

Correct: B

Why B is correct: File.WriteAllText replaces the entire contents of the file with the new text every time it's called — exactly the mistake described in the lesson when someone means to keep adding log lines over time.

Why A is incorrect: AppendAllText is specifically designed to add to the end of the file without touching existing content — it's the correct choice for this scenario, not the mistake.

Why C is incorrect: Directory.CreateDirectory works on folders, not files — calling it with a file-like name would attempt to create a directory, an unrelated and likely error-prone operation.

Why D is incorrect: Exists is a read-only check — it never modifies file contents at all.

Reinforcement: WriteAllText always overwrites; AppendAllText always adds. Pick based on whether you want to replace or accumulate.

4. In the order-import example, why does the code move a failed CSV file into a separate "failed" folder inside a catch block, instead of just letting the exception propagate and stop the whole job?

Show answer

Correct: B

Why B is correct: This is the per-item error containment pattern from earlier lessons applied to files: catching the exception for just this one file lets the loop move on to the next file rather than aborting the entire batch, and quarantining the bad file preserves it for someone to investigate later.

Why A is incorrect: Directory.GetFiles just returns a list of paths — it has no requirement about what you do with each one afterward.

Why C is incorrect: File.Move only relocates a file on disk — it has no awareness of, or effect on, the file's contents or formatting.

Why D is incorrect: The loop would compile fine without a try/catch — the catch block is a deliberate design choice for resilience, not a compiler requirement.

Reinforcement: Containing failure to the smallest reasonable scope — one file, one row, one item — keeps a batch operation resilient instead of all-or-nothing.

5. You need to process a 20 GB log file line by line without loading the whole thing into memory at once. Is File.ReadAllText a good fit here?

Show answer

Correct: B

Why B is correct: ReadAllText reads the entire file into a single in-memory string in one call — for a 20 GB file, that's rarely practical. This is exactly the boundary the lesson draws: File/Directory are great for whole small-to-medium files, but large or incremental data calls for Streams, covered next.

Why A is incorrect: It's the opposite — ReadAllText is best suited to small-to-moderate files that comfortably fit in memory.

Why C is incorrect: There's no automatic size-based switch to incremental reading — ReadAllText always loads the full content at once, regardless of file size.

Why D is incorrect: ReadAllText works with any text file, not just CSVs — file extension isn't the deciding factor here at all.

Reinforcement: Whole-file methods are for content that reasonably fits in memory; very large or incremental data needs a Stream.

You can now read, write, and organize files and directories with real error handling in mind. Next up: the Path class — because building file paths by hand is a surprisingly common source of bugs.


dotnetmadeeasy.com — Learn C# and .NET, the right way.