13.1. Error Handling and Validation#

Programs fail in predictable ways: bad input, missing files, unexpected values, and invalid assumptions. Good software does not pretend errors never happen — it handles them deliberately.

13.1.1. Validation vs. Exceptions#

  • Validation checks expected bad input before doing risky work.

  • Exceptions handle unexpected failures at runtime.

Validation should be your first line of defense. Exceptions are a fallback for when something still goes wrong.

13.1.2. Basic try / catch / finally#

Use finally for cleanup (closing resources, resetting state):

try
{
    Console.Write("Enter a number: ");
    int value = int.Parse(Console.ReadLine()!);
    Console.WriteLine($"You entered {value}");
}
catch (FormatException)
{
    Console.WriteLine("Input must be a whole number.");
}
catch (OverflowException)
{
    Console.WriteLine("Number is outside the valid int range.");
}
finally
{
    Console.WriteLine("Input attempt complete.");
}

13.1.3. Prefer TryParse for User Input#

If invalid input is expected, avoid throwing exceptions entirely:

Console.Write("Enter age: ");
string raw = Console.ReadLine() ?? "";

if (int.TryParse(raw, out int age) && age >= 0)
{
    Console.WriteLine($"Age recorded: {age}");
}
else
{
    Console.WriteLine("Please enter a non-negative whole number.");
}

Guideline:

  • Use TryParse for normal input flow where bad input is common.

  • Use try/catch around operations that can fail unpredictably (file I/O, network, external dependencies).

13.1.4. File I/O with Safe Handling#

Catch specific exception types before broad ones:

string path = "scores.txt";

try
{
    using StreamReader reader = new StreamReader(path);
    string? line;
    while ((line = reader.ReadLine()) != null)
        Console.WriteLine(line);
}
catch (FileNotFoundException)
{
    Console.WriteLine($"Could not find file: {path}");
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("Permission denied while reading the file.");
}
catch (IOException ex)
{
    Console.WriteLine($"I/O error: {ex.Message}");
}

13.1.5. Throwing Exceptions Intentionally#

Throw when an API is used incorrectly and execution should stop:

static double Divide(double numerator, double denominator)
{
    if (denominator == 0)
        throw new ArgumentException("Denominator cannot be zero.", nameof(denominator));

    return numerator / denominator;
}

Console.WriteLine(Divide(10, 2));   // 5
Console.WriteLine(Divide(10, 0));   // throws

This is better than returning meaningless sentinel values that hide bugs.

13.1.6. Error Message Quality#

Bad

Better

"Error happened"

"Could not open 'scores.txt'. Check that the file exists and that you have read permission."

Good error messages are: specific, user-actionable, and free of raw internal noise.

13.1.7. Common Mistakes#

  • Catching Exception everywhere and ignoring details

  • Swallowing exceptions without logging or reporting

  • Using exceptions for normal control flow (use TryParse instead)

  • Showing stack traces directly to end users

13.1.8. Practice#

  1. Read a value from the console and keep prompting until a valid double is entered.

  2. Open a file path provided by the user; print a friendly message for: file not found, access denied, and unknown I/O error.

  3. Add argument checks to one of your existing methods and throw ArgumentException when input is invalid.

Footnotes