C# (.NET 8 LTS) track

Lesson 16 · Part 4: Exceptions & I/O

File I/O

Read and write text files with File, StreamReader and Path while handling I/O failures.

Part 4: Exceptions & I/O4 min readFree lesson

Reading configuration files, writing logs, and parsing CSV are done by almost every app. C# provides a rich API for handling files with the System.IO namespace. Short operations are handled by the File static method, while larger files are handled by the Stream family.

Learning Objectives

  • Know how to use File.ReadAllText/WriteAllText/ReadAllLines/WriteAllLines
  • Learn the pattern of reading line by line with StreamReader
  • Manipulate OS-independent paths with the Path class
  • Create a safe temporary file with Path.GetTempFileName()
  • Experience the basics of async asynchronous file I/O

Key concepts

1) File static method (for small files)

This is the simplest way to read and write the entire file at once.

File.WriteAllText("a.txt", "hello");           //Write entire string
string text = File.ReadAllText("a.txt");      //read full

File.WriteAllLines("b.txt", new[] {"a","b"}); //write line by line
string[] lines = File.ReadAllLines("b.txt");  //Read line by line (array)

If it's more than a few tens of MB, the stream method is better because it eats up all the memory.

2) StreamReader for large files

Reading and processing line by line uses less memory. Since it is IDisposable, wrap it as using.

using var sr = new StreamReader(path);
string? line;
while ((line = sr.ReadLine()) is not null)
{
    //line processing
}

3) Path — path manipulation

Instead of connecting directly to "/" or "\\", use Path.Combine. This is because the separator is different for each OS.

string p = Path.Combine("logs", "2025", "app.log");
string name = Path.GetFileName(p);       // "app.log"
string ext  = Path.GetExtension(p);      // ".log"
string tmp  = Path.GetTempFileName();    //Create empty file in OS temporary folder

Path.GetTempFileName() automatically creates conflict-free files. It is safe to use this as a learning example.

4) Handling I/O exceptions

File operations almost always have the potential to fail (missing file, insufficient permissions, etc.).

try
{
    string s = File.ReadAllText(path);
}
catch (FileNotFoundException)   { /* ... */ }
catch (UnauthorizedAccessException) { /* ... */ }
catch (IOException) { /*Other I/O errors*/ }

5) Asynchronous I/O (async/await)

Disks are slow. In the UI or server, it is processed asynchronously and does not block other operations. This is covered in detail in Part 19, but the usage is simple — the method name is followed by Async and is received as await.

string s = await File.ReadAllTextAsync(path);
await File.WriteAllTextAsync(path, s);

Full-scale async/await learning is covered in Part 19. Just learn the shape here.

See as example

Example 1 — WriteRead: Write and read entire text at once

string path = Path.GetTempFileName();           //temporary files

File.WriteAllText(path, "Hello, file!\nSecond line");

string text = File.ReadAllText(path);
Console.WriteLine("== What you read ==");
Console.WriteLine(text);

File.Delete(path);                              //organizing

Execution result

== What you read ==
Hello, File!
second line

Note: Path.GetTempFileName() creates conflict-free files in the OS temporary folder.

Example 2 — WriteReadLines: Dealing with arrays of lines

string path = Path.GetTempFileName();

string[] names = ["Alice", "Bob", "Charlie"];
File.WriteAllLines(path, names);

string[] loaded = File.ReadAllLines(path);
foreach (var n in loaded)
    Console.WriteLine($"- {n}");

File.Delete(path);

Execution result

- Alice
- Bob
- Charlie

Memo: The cleanest line-by-line data is WriteAllLines/ReadAllLines.

Example 3 — StreamReaderUse: Stream line by line

string path = Path.GetTempFileName();
File.WriteAllLines(path, ["apple", "banana", "cherry"]);

//Explicitly limit the lifetime of sr with a using block.
using (var sr = new StreamReader(path))
{
    int lineNo = 1;
    string? line;
    while ((line = sr.ReadLine()) is not null)
    {
        Console.WriteLine($"{lineNo}: {line}");
        lineNo++;
    }
}

File.Delete(path);

Execution result

1: apple
2: Banana
3: Cherry

Note: Memory usage is consistent even with large files. When you exit the using block, the handle is closed immediately, and the immediately following File.Delete does not get caught in Windows' file lock (if it were using var, the handle would be maintained until the end of the method scope, causing a crash).

Example 4 — PathHelpers: Splitting and merging paths

string combined = Path.Combine("logs", "2025", "app.log");
Console.WriteLine($"Combine    : {combined}");
Console.WriteLine($"FileName   : {Path.GetFileName(combined)}");
Console.WriteLine($"Extension  : {Path.GetExtension(combined)}");
Console.WriteLine($"NoExt      : {Path.GetFileNameWithoutExtension(combined)}");

string temp = Path.GetTempFileName();
Console.WriteLine($"Temp       : {temp}");
File.Delete(temp);

Execution result

Combine    : logs/2025/app.log
FileName   : app.log
Extension  : .log
NoExt      : app
Temp       : /tmp/tmpXXXXXX.tmp

Memo: In Windows, \ is entered. Do not connect manually, use Path.Combine.

Example 5 — AsyncFile: Asynchronous read and write

//top-level await: Available starting from C# 9 / .NET 5+
string path = Path.GetTempFileName();

await File.WriteAllTextAsync(path, "Save asynchronously!");
string text = await File.ReadAllTextAsync(path);

Console.WriteLine($"Read: {text}");

File.Delete(path);

Execution result

Read: Save asynchronously!

Note: The behavior is the same as the synchronous version, but you can do other things while the disk is waiting. This is covered in detail in Part 19.

Common Mistakes

  1. Directly connect the path to "a" + "/" + "b" — OS dependent. Use Path.Combine.
  2. Reading large files with ReadAllText — memory explosion. Line by line as a stream.
  3. Using StreamReader without using — file handle leak.
  4. Placing temporary files in your working directory — polluting your environment. Use Path.GetTempFileName().
  5. Missing exception handling — Prepare for missing files, permissions, disk full, etc.

Cleanup

  • Small text: File.ReadAllText/WriteAllText
  • Joule unit: File.ReadAllLines/WriteAllLines
  • Large files: StreamReader + using
  • The path is Path.Combine and the temporary file is Path.GetTempFileName().
  • In places where the disk is slow, *Async version + await (Preview of Part 19)

Try it yourself

cd src/WriteRead && dotnet run
cd ../WriteReadLines && dotnet run
cd ../StreamReaderUse && dotnet run
cd ../PathHelpers && dotnet run
cd ../AsyncFile && dotnet run

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/AsyncFile/AsyncFile.csprojText
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.IoEx16</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
src/AsyncFile/Program.csC#
//Asynchronous file I/O (top-level await — C# 9 / .NET 5+)
//Full-scale async/await is covered in Part 19.
string path = Path.GetTempFileName();

await File.WriteAllTextAsync(path, "Save asynchronously!");
string text = await File.ReadAllTextAsync(path);

Console.WriteLine($"Read: {text}");

File.Delete(path);
src/PathHelpers/PathHelpers.csprojText
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.IoEx16</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
src/PathHelpers/Program.csC#
//Path utility methods
string combined = Path.Combine("logs", "2025", "app.log");
Console.WriteLine($"Combine    : {combined}");
Console.WriteLine($"FileName   : {Path.GetFileName(combined)}");
Console.WriteLine($"Extension  : {Path.GetExtension(combined)}");
Console.WriteLine($"NoExt      : {Path.GetFileNameWithoutExtension(combined)}");

string temp = Path.GetTempFileName();
Console.WriteLine($"Temp       : {temp}");
File.Delete(temp);
src/StreamReaderUse/Program.csC#
//StreamReader: Stream line by line (large file friendly)
string path = Path.GetTempFileName();
File.WriteAllLines(path, ["apple", "banana", "cherry"]);

//Explicitly limit the lifespan of sr with a using block → Dispose as soon as you exit the block.
//If you use `using var`, the handle is maintained until the end of the file method scope.
//In Windows, File.Delete below locks the file.
using (var sr = new StreamReader(path))
{
    int lineNo = 1;
    string? line;
    while ((line = sr.ReadLine()) is not null)
    {
        Console.WriteLine($"{lineNo}: {line}");
        lineNo++;
    }
}

//After sr is Disposed, the file can be safely deleted.
File.Delete(path);
src/StreamReaderUse/StreamReaderUse.csprojText
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.IoEx16</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
src/WriteRead/Program.csC#
//Write full text to temporary file and read back
string path = Path.GetTempFileName();

File.WriteAllText(path, "Hello, file!\nSecond line");

string text = File.ReadAllText(path);
Console.WriteLine("== What you read ==");
Console.WriteLine(text);

File.Delete(path);                          //Leave no trace in working directory
src/WriteRead/WriteRead.csprojText
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.IoEx16</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
src/WriteReadLines/Program.csC#
//Write and read line by line at once
string path = Path.GetTempFileName();

string[] names = ["Alice", "Bob", "Charlie"];
File.WriteAllLines(path, names);

string[] loaded = File.ReadAllLines(path);
foreach (var n in loaded)
{
    Console.WriteLine($"- {n}");
}

File.Delete(path);
src/WriteReadLines/WriteReadLines.csprojText
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.IoEx16</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Assignment

Problem 1 — Numbering and Printing

  • Reference solution: shown below
  • Core concepts: Path.GetTempFileName, File.WriteAllLines, File.ReadAllLines

Requirements

  • Create a temporary file (Path.GetTempFileName())
  • Write ["Seoul", "Busan", "Daegu", "Gwangju"] line by line to a temporary file.
  • Read the file and print it line by line with a number like [1] Seoul
  • Delete files before termination

Expected output

[1] Seoul
[2] Busan
[3] Daegu
[4] Gwangju

Hint

  • Use File.WriteAllLines(path, lines) and File.ReadAllLines(path) as is
  • Index is for (int i = 0; i < lines.Length; i++) or Enumerate pattern

Problem 2 — CSV Column Sum

  • Reference solution: shown below
  • Key concepts: line/field parsing, numeric conversion, cumulative sum

Requirements

  • Write the following CSV to a temporary file (header + 3 lines):
    name,score
    Alice,80
    Bob,95
    Charlie,72
    
  • Read the file line by line, skip the header, and add the second column (score)
  • Prints the sum and average (OK with integer division)

Expected output

Total: 247
Average: 82

Hint

  • Cut the column with string.Split(',')
  • Convert numbers to int.Parse(parts[1])
  • The header just skips the first line (i == 0) to continue;.

Check the correct answer

After solving it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

Homework01/Homework01.csprojText
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.IoEx16</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
Homework01/Program.csC#
//Write to temporary file, numbered output
string path = Path.GetTempFileName();

string[] cities = ["Seoul", "Busan", "Daegu", "Gwangju"];
File.WriteAllLines(path, cities);

string[] loaded = File.ReadAllLines(path);
for (int i = 0; i < loaded.Length; i++)
{
    Console.WriteLine($"[{i + 1}] {loaded[i]}");
}

File.Delete(path);
Homework02/Homework02.csprojText
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.IoEx16</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
Homework02/Program.csC#
//Create a CSV temporary file and calculate score column sum/average
string path = Path.GetTempFileName();

string[] csv =
[
    "name,score",
    "Alice,80",
    "Bob,95",
    "Charlie,72",
];
File.WriteAllLines(path, csv);

string[] lines = File.ReadAllLines(path);
int total = 0;
int count = 0;

for (int i = 0; i < lines.Length; i++)
{
    if (i == 0) continue;                       //Skip header
    string[] parts = lines[i].Split(',');
    total += int.Parse(parts[1]);
    count++;
}

Console.WriteLine($"Total: {total}");
Console.WriteLine($"Average: {total / count}");

File.Delete(path);

Learn this lesson with a mentor

Work through the C# (.NET 8 LTS) track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now