Why File I/O Matters in Console Apps
Console apps often need to persist data between runs: saving user preferences, caching results, exporting reports, or keeping a simple log. In .NET, the System.IO namespace provides straightforward APIs for reading and writing files. This chapter focuses on safe, practical scenarios using the static File class for common tasks.
Working Directory, Relative Paths, and Absolute Paths
What is the working directory?
When you use a relative path (like data.txt or data\notes.txt), .NET resolves it relative to the process working directory (also called the current directory). In a console app, this is commonly the folder where the app is launched from (often the build output folder when running from an IDE, but it can differ when launched from a terminal).
You can inspect it at runtime:
using System;using System.IO;Console.WriteLine("Current directory: " + Directory.GetCurrentDirectory());Relative paths
Relative paths are short and convenient, but they can be confusing if the working directory changes. Examples:
data.txt(in the current directory)data\data.txt(in a subfolder nameddata)..\data.txt(one folder up)
Absolute paths
Absolute paths fully specify the location. Examples:
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
- Windows:
C:\Users\Ava\Documents\data.txt - macOS/Linux:
/Users/ava/Documents/data.txt
Absolute paths are explicit and predictable, but less portable across machines.
Making paths explicit (recommended)
A practical compromise is to build an explicit path based on a known base directory. Two common choices are:
- Current directory (explicitly retrieved):
Directory.GetCurrentDirectory() - App base directory (where the executable is located):
AppContext.BaseDirectory
Use Path.Combine to join path segments safely across operating systems:
using System;using System.IO;string baseDir = AppContext.BaseDirectory;string filePath = Path.Combine(baseDir, "data", "notes.txt");Console.WriteLine(filePath);If you write to a subfolder, ensure it exists:
using System.IO;string folderPath = Path.Combine(AppContext.BaseDirectory, "data");Directory.CreateDirectory(folderPath);Reading Files
Reading the entire file as a single string: File.ReadAllText
File.ReadAllText loads the whole file into memory as one string. It’s ideal for small configuration files, templates, or saved text.
using System.IO;string text = File.ReadAllText("data.txt");Console.WriteLine(text);Notes:
- If the file doesn’t exist, it throws
FileNotFoundException. - If the path is invalid, it can throw
ArgumentExceptionorNotSupportedException. - If access is denied, it can throw
UnauthorizedAccessException.
Reading lines into an array: File.ReadAllLines
File.ReadAllLines reads a text file and returns a string[], one element per line. This is convenient for simple line-based formats (lists, logs, one-record-per-line data).
using System.IO;string[] lines = File.ReadAllLines("data.txt");foreach (string line in lines){ Console.WriteLine("- " + line);}Writing Files
Overwriting (or creating) a file: File.WriteAllText
File.WriteAllText creates a file if it doesn’t exist, or overwrites it if it does. Use it when you want the file to represent the latest state.
using System.IO;File.WriteAllText("data.txt", "Hello from .NET!");Appending to a file: File.AppendAllText
File.AppendAllText adds text to the end of a file (and creates it if missing). It’s useful for logs or accumulating entries.
using System.IO;File.AppendAllText("log.txt", "Started at: " + DateTime.Now + Environment.NewLine);Environment.NewLine inserts the correct line ending for the current OS.
Handling Missing Files and Invalid Paths Safely
File I/O is inherently error-prone: files may not exist, paths may be malformed, or permissions may block access. For console apps, aim for clear messages and safe fallbacks.
Step 1: Validate and build a safe path
Prefer building paths with Path.Combine and avoid letting raw user input become a full path without checks. If you accept a filename from the user, treat it as a name (not a path), and store it in a known folder.
using System;using System.IO;string folder = Path.Combine(AppContext.BaseDirectory, "data");Directory.CreateDirectory(folder);Console.Write("Enter a file name (no path): ");string fileName = Console.ReadLine() ?? "";foreach (char c in Path.GetInvalidFileNameChars()){ fileName = fileName.Replace(c.ToString(), "_");}if (string.IsNullOrWhiteSpace(fileName)) fileName = "default.txt";if (!fileName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) fileName += ".txt";string path = Path.Combine(folder, fileName);Console.WriteLine("Will use: " + path);Step 2: Check existence when reading
Before reading, you can check File.Exists and decide what to do if it’s missing.
using System.IO;string path = "data.txt";if (!File.Exists(path)){ Console.WriteLine("No saved file found yet.");}else{ string text = File.ReadAllText(path); Console.WriteLine(text);}Step 3: Use structured exception handling for I/O
Even with checks, errors can still happen (race conditions, permissions, locked files). Catch specific exceptions and provide actionable output.
using System;using System.IO;string path = "data.txt";try{ string text = File.ReadAllText(path); Console.WriteLine(text);}catch (FileNotFoundException){ Console.WriteLine("The file was not found: " + path);}catch (DirectoryNotFoundException){ Console.WriteLine("The folder in the path does not exist.");}catch (UnauthorizedAccessException){ Console.WriteLine("Access denied. Try a different folder or run with appropriate permissions.");}catch (ArgumentException){ Console.WriteLine("The path is invalid.");}catch (IOException ex){ Console.WriteLine("An I/O error occurred: " + ex.Message);}For writing, similar exceptions can occur. If you write to a subfolder, create it first with Directory.CreateDirectory.
Exercise: Save User-Entered Data, Load It Next Run, Display Results
Goal: Build a small console app that stores a list of user-entered items in a text file. On startup, it loads existing items (if any) and displays them. Then it asks for new items, saves them, and exits.
Rules for the exercise
- Store the file in a known location relative to the app: a
datafolder underAppContext.BaseDirectory. - Use one item per line in the file.
- Use
File.ReadAllLinesto load andFile.WriteAllTextto save (orAppendAllTextif you choose to append). - Handle missing file and common path/permission errors.
Step-by-step implementation
Step 1: Decide where the file will be created
We’ll create data/items.txt under the app base directory. This makes the location explicit and consistent.
using System;using System.IO;string dataFolder = Path.Combine(AppContext.BaseDirectory, "data");Directory.CreateDirectory(dataFolder);string filePath = Path.Combine(dataFolder, "items.txt");Console.WriteLine("Data file: " + filePath);Step 2: Load existing items (if the file exists)
using System;using System.IO;string[] existingItems = Array.Empty<string>();try{ if (File.Exists(filePath)){ existingItems = File.ReadAllLines(filePath); }}catch (UnauthorizedAccessException){ Console.WriteLine("Cannot read the data file due to permissions.");}catch (IOException ex){ Console.WriteLine("Could not read the data file: " + ex.Message);}Step 3: Display what was loaded
using System;Console.WriteLine();Console.WriteLine("Saved items:");if (existingItems.Length == 0){ Console.WriteLine("(none yet)");}else{ for (int i = 0; i < existingItems.Length; i++){ Console.WriteLine($"{i + 1}. {existingItems[i]}"); }}Step 4: Collect new items from the user
Let the user enter items until they submit an empty line.
using System;var newItems = new System.Collections.Generic.List<string>();Console.WriteLine();Console.WriteLine("Enter new items (blank line to finish):");while (true){ Console.Write("> "); string input = Console.ReadLine() ?? ""; if (string.IsNullOrWhiteSpace(input)) break; newItems.Add(input.Trim());}Step 5: Save the combined list back to the file
We’ll overwrite the file with the updated list to keep it simple and deterministic.
using System;using System.IO;var allItems = new System.Collections.Generic.List<string>();allItems.AddRange(existingItems);allItems.AddRange(newItems);try{ string output = string.Join(Environment.NewLine, allItems); File.WriteAllText(filePath, output); Console.WriteLine(); Console.WriteLine("Saved " + allItems.Count + " item(s).");}catch (UnauthorizedAccessException){ Console.WriteLine("Cannot write the data file due to permissions.");}catch (DirectoryNotFoundException){ Console.WriteLine("The data folder was not found.");}catch (IOException ex){ Console.WriteLine("Could not write the data file: " + ex.Message);}Full example (put into Program.cs)
using System;using System.IO;using System.Collections.Generic;string dataFolder = Path.Combine(AppContext.BaseDirectory, "data");Directory.CreateDirectory(dataFolder);string filePath = Path.Combine(dataFolder, "items.txt");Console.WriteLine("Data file: " + filePath);string[] existingItems = Array.Empty<string>();try{ if (File.Exists(filePath)){ existingItems = File.ReadAllLines(filePath); }}catch (UnauthorizedAccessException){ Console.WriteLine("Cannot read the data file due to permissions.");}catch (IOException ex){ Console.WriteLine("Could not read the data file: " + ex.Message);}Console.WriteLine();Console.WriteLine("Saved items:");if (existingItems.Length == 0){ Console.WriteLine("(none yet)");}else{ for (int i = 0; i < existingItems.Length; i++){ Console.WriteLine($"{i + 1}. {existingItems[i]}"); }}var newItems = new List<string>();Console.WriteLine();Console.WriteLine("Enter new items (blank line to finish):");while (true){ Console.Write("> "); string input = Console.ReadLine() ?? ""; if (string.IsNullOrWhiteSpace(input)) break; newItems.Add(input.Trim());}var allItems = new List<string>();allItems.AddRange(existingItems);allItems.AddRange(newItems);try{ string output = string.Join(Environment.NewLine, allItems); File.WriteAllText(filePath, output); Console.WriteLine(); Console.WriteLine("Saved " + allItems.Count + " item(s).");}catch (UnauthorizedAccessException){ Console.WriteLine("Cannot write the data file due to permissions.");}catch (DirectoryNotFoundException){ Console.WriteLine("The data folder was not found.");}catch (IOException ex){ Console.WriteLine("Could not write the data file: " + ex.Message);}Common Pitfalls and Practical Tips
Know where your file is being created
If you use a relative path like items.txt, it will be created in the current working directory, which may not be the same as the executable folder. Printing Directory.GetCurrentDirectory() and printing the full file path you’re using are simple debugging tools.
Prefer Path.Combine over manual separators
Use Path.Combine instead of hardcoding \ or /. It avoids subtle bugs and improves portability.
Choose overwrite vs append intentionally
- Use
WriteAllTextwhen the file should match your current state. - Use
AppendAllTextwhen you want a running history (like a log).
Handle errors with clear messages
In a console app, the best user experience is usually: explain what failed, show the path involved, and suggest a next step (choose a different folder, check permissions, create the missing directory).