Menu

C# Write to File and Read File: File, StreamWriter, StreamReader

How to write to a file and read a file in C#: File.WriteAllText, ReadAllText, AppendAllText, WriteAllLines and ReadAllLines for whole files, StreamWriter and StreamReader for large ones, paths with Path.Combine, directories, encodings, and the exceptions file code has to handle.

This page includes runnable editors - edit, run, and see output instantly.

The System.IO namespace has everything for working with files. For most tasks, one call on the static File class is enough: it opens the file, reads or writes it, and closes it. For large files or fine control, use StreamReader and StreamWriter. Add using System.IO; to every example below.

Write and read a whole file

File.WriteAllText creates a file (or overwrites an existing one) with the text you give it. File.ReadAllText returns the whole file as one string. File.AppendAllText adds to the end:

Output:

True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False

A relative path such as "notes.txt" is resolved against the process's current directory, which is not always the folder your program lives in (a service, a scheduled task or a test runner can start elsewhere). For files that ship with your application, build the path from AppContext.BaseDirectory.

WriteAllText overwrites without asking. If losing the old content matters, check File.Exists first or write to a temporary file and then File.Move it into place.

Lines: WriteAllLines, ReadAllLines and ReadLines

Files of records, one per line, are common enough to have their own methods. WriteAllLines writes each string followed by a newline; ReadAllLines returns a string[]:

Output:

4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00

The difference between the two readers matters for size. ReadAllLines reads the whole file into memory before returning. ReadLines returns a lazy IEnumerable<string> that reads as you iterate, so a foreach over a 5 GB log uses a few kilobytes. With LINQ, ReadLines(...).Where(...).Take(10) stops reading after it has ten matches.

Parsing CSV with Split(',') works for simple files you produced yourself. Real-world CSV has quoted fields containing commas; use a library such as CsvHelper for those.

Notice the InvariantCulture in decimal.Parse: without it, parsing "4.50" on a machine set to German or Portuguese reads the dot as a thousands separator.

StreamWriter and StreamReader

The File methods open and close the file on every call. To write many pieces over time, or to read a file too large for memory without LINQ, open a stream once. Always wrap it in using so it is closed even if an exception is thrown; an unclosed writer may never flush its buffer, and the file ends up empty or cut short.

Output:

2: WARN  disk 85% full
4: ERROR connection refused

ReadLine returns null at the end of the file, which is what ends the while loop. A StreamWriter buffers its output and writes it to disk when the buffer fills, when you call Flush(), and when it is disposed; the using block guarantees the last one.

Since C# 8, a using declaration disposes the stream at the end of the enclosing block, without the extra braces:

using var writer = new StreamWriter(path);   // C# 8: disposed when the method returns
writer.WriteLine("INFO  server started");

Paths and directories

Build paths with Path.Combine rather than string concatenation. It inserts the right separator for the operating system (\ on Windows, / on Linux and macOS) and does not double it up. The Path class also takes paths apart, and Directory creates and lists folders:

Output:

february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False

One trap: if a later argument to Path.Combine is rooted (it starts with / or \, or a drive letter on Windows), everything before it is dropped, so Path.Combine("reports", "/2026") is /2026. Pass relative parts, or use Path.Join (.NET Core 3.0 and later), which never drops anything.

Directory.GetFiles returns files in whatever order the file system gives them, which differs between Windows and Linux, so sort the result when order matters. Directory.EnumerateFiles is the lazy version, the same relationship as ReadLines to ReadAllLines. Pass SearchOption.AllDirectories to include subfolders.

Handling errors

File operations fail for reasons outside your program: the file is missing, the folder does not exist, another process has it open, the disk is full, permissions are wrong. Each has its own exception type. FileNotFoundException and DirectoryNotFoundException derive from IOException, so catch them before it; UnauthorizedAccessException does not, so it needs its own catch:

Output:

missing file, using defaults
missing folder, using defaults
theme=dark

File.Exists before a read seems simpler, but it does not remove the need for the try: the file can disappear, or be locked by another program, in the moment between the check and the read. Use Exists to decide what to do, and catch to survive what actually happens.

Encoding

File.WriteAllText, WriteAllLines and StreamWriter write UTF-8 without a byte order mark by default, and the readers detect UTF-8, UTF-16 and UTF-32 from a byte order mark if one is present. Pass an Encoding when a file must be in a specific format:

Output:

plain.txt   16 bytes, reads back unchanged: True
bom.txt     19 bytes, reads back unchanged: True
utf16.txt   30 bytes, reads back unchanged: True

é and ã take two bytes each in UTF-8, which is why 14 characters need 16 bytes, and all three files read back correctly because the reader recognizes the byte order mark. Encoding.UTF8 looks like the default but adds a byte order mark, which some tools (older CSV importers, shell scripts) show as garbage at the start of the first line; use new UTF8Encoding(false) for explicit UTF-8 without one. Reading a file in the wrong encoding does not throw: it produces replacement characters, so find out what encoding a file you did not create uses.

Async file I/O

In web apps and UI code, blocking a thread on disk I/O wastes it. .NET Core 2.0 and later have async versions of the File methods:

await File.WriteAllTextAsync("notes.txt", text);
string content = await File.ReadAllTextAsync("notes.txt");
string[] lines = await File.ReadAllLinesAsync("sales.csv");

StreamReader.ReadLineAsync and StreamWriter.WriteLineAsync work the same way on streams.

Common mistakes

  • Not disposing a stream. Without using, buffered text may never reach the disk and the file stays locked until the garbage collector runs.
  • Building paths with + "\\" +. It breaks on Linux and macOS. Use Path.Combine.
  • Loading huge files with ReadAllText or ReadAllLines. Stream them with ReadLines or a StreamReader.
  • Relying on the current directory. It depends on how the program was started. Use absolute paths or AppContext.BaseDirectory.
  • Parsing numbers from files with the machine's culture. Pass CultureInfo.InvariantCulture for data files.
  • Expecting File.Exists to prevent exceptions. The file can change between the check and the use.

Frequently Asked Questions

How do I write text to a file in C#?

File.WriteAllText("notes.txt", text); creates the file, or overwrites it if it exists, writes the string as UTF-8, and closes it. Use File.WriteAllLines(path, lines) for a collection of lines and File.AppendAllText(path, text) to add to the end instead of replacing. All three live in System.IO.

How do I read a text file in C#?

string text = File.ReadAllText("notes.txt"); reads the whole file into one string, and string[] lines = File.ReadAllLines(path); splits it into lines. For large files use File.ReadLines(path) in a foreach, which reads one line at a time instead of loading everything, or a StreamReader.

How do I append to a file in C#?

File.AppendAllText(path, text) adds text to the end of the file and creates the file if it does not exist; File.AppendAllLines(path, lines) does the same for lines. With a stream, open it in append mode: new StreamWriter(path, append: true). Remember to add Environment.NewLine or \n yourself when appending text.

How do I read a file line by line in C#?

foreach (string line in File.ReadLines(path)) { ... } streams the file one line at a time, so memory use stays flat even for gigabyte logs. The older equivalent is a StreamReader in a using block with while ((line = reader.ReadLine()) != null). Avoid ReadAllLines for large files: it loads every line into an array first.

How do I check if a file exists in C#?

File.Exists(path) returns true if the file exists and the program may see it, and Directory.Exists(path) does the same for folders. Do not rely on it to avoid exceptions: the file can be deleted or locked between the check and the read, so still handle FileNotFoundException and IOException around the actual file operation.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED