Free Ebook cover C# Essentials: Building Console Apps with .NET

C# Essentials: Building Console Apps with .NET

New course

12 pages

Basic Error Handling and Defensive Coding in C#

Capítulo 9

Estimated reading time: 7 minutes

+ Exercise

Why Console Apps Fail: Exceptions and Defensive Coding

Console apps often fail at the boundaries where they interact with the outside world: user input, files, and the environment. In .NET, many failures are represented as exceptions: objects that describe an error condition and interrupt normal execution unless handled.

Defensive coding means anticipating invalid input and unreliable resources, then writing code that either prevents errors (validation) or handles them gracefully (exception handling). In this chapter, you will improve an existing program by first observing how it fails, then adding guarded parsing, wrapping risky file operations, and finally producing user-friendly error messages.

Starting Point: A Program That Fails in Common Ways

Assume you have a small console tool that asks for an age and then reads a text file and prints it. It works in the happy path, but it is fragile.

using System;using System.IO;class Program{    static void Main()    {        Console.Write("Enter your age: ");        int age = int.Parse(Console.ReadLine());        Console.Write("Enter path to a text file: ");        string path = Console.ReadLine();        string text = File.ReadAllText(path);        Console.WriteLine($"You are {age}. File length: {text.Length}");    }}

Observe the failures

  • If the user types abc for age, int.Parse throws FormatException.
  • If the user presses Enter without typing anything, int.Parse can throw ArgumentNullException (because ReadLine() can return null).
  • If the file path is wrong or inaccessible, File.ReadAllText can throw FileNotFoundException, DirectoryNotFoundException, UnauthorizedAccessException, or a general IOException.

These exceptions are not “bad”; they are signals. The goal is to decide which ones you can recover from and how to communicate the problem to the user.

Step 1: Guarded Parsing with TryParse-Style APIs

For user input, prefer TryParse-style APIs when invalid input is expected. They avoid exceptions for common validation failures and keep control flow explicit.

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 App

Download the app

Replace int.Parse with int.TryParse

Console.Write("Enter your age: ");string? ageText = Console.ReadLine();if (!int.TryParse(ageText, out int age)) {    Console.WriteLine("Please enter a whole number for age.");    return;}

This change prevents FormatException for typical invalid input. It also handles null and empty strings because TryParse returns false rather than throwing.

Add range checks (defensive validation)

TryParse only validates the format. You often also need domain validation.

if (age < 0 || age > 130){    Console.WriteLine("Age must be between 0 and 130.");    return;}

Practical step-by-step: keep prompting until valid

Instead of exiting, you can loop until the user provides valid input.

int age;while (true){    Console.Write("Enter your age: ");    string? ageText = Console.ReadLine();    if (!int.TryParse(ageText, out age))    {        Console.WriteLine("Invalid number. Try again.");        continue;    }    if (age < 0 || age > 130)    {        Console.WriteLine("Out of range. Try again.");        continue;    }    break;}

Use this approach when the program can reasonably continue and the user can correct the input.

Step 2: try/catch/finally for Risky Operations

Use try/catch when you are calling code that can throw exceptions and you have a meaningful recovery strategy (retry, fallback, alternate path) or you can present a helpful message and exit cleanly.

Wrap file reads and catch specific exceptions

Console.Write("Enter path to a text file: ");string? path = Console.ReadLine();try{    string text = File.ReadAllText(path);    Console.WriteLine($"You are {age}. File length: {text.Length}");}catch (ArgumentException){    Console.WriteLine("The path is not valid.");}catch (UnauthorizedAccessException){    Console.WriteLine("Access denied. Try a different file or run with appropriate permissions.");}catch (FileNotFoundException){    Console.WriteLine("File not found. Check the path and try again.");}catch (DirectoryNotFoundException){    Console.WriteLine("Directory not found. Check the folder path.");}catch (IOException ex){    Console.WriteLine($"File I/O error: {ex.Message}");}

Catching specific exceptions first lets you tailor messages and actions. A broader IOException catch can cover other I/O problems (sharing violations, device errors), but you still keep it scoped to file-related failures.

When to use finally

finally runs whether an exception occurs or not. Use it for cleanup that must happen even if something fails (closing resources, restoring state, stopping timers). Many file APIs handle cleanup internally, but finally is still useful for your own resources.

var start = DateTime.UtcNow;try{    // Risky work here    string text = File.ReadAllText(path);    Console.WriteLine(text);}catch (IOException ex){    Console.WriteLine(ex.Message);}finally{    var elapsed = DateTime.UtcNow - start;    Console.WriteLine($"Operation took {elapsed.TotalMilliseconds} ms");}

Step 3: Improve User-Friendly Error Messages Without Hiding Useful Details

A good console app message tells the user what happened and what to do next. Avoid dumping stack traces for expected problems (like invalid input), but keep enough detail for troubleshooting when appropriate.

Differentiate expected vs unexpected errors

  • Expected: invalid number, missing file, access denied. Handle with clear guidance and allow retry when reasonable.
  • Unexpected: bugs (null references, logic errors), corrupted state. Consider letting these bubble up during development, or log details and show a generic message in production tools.

Example: retry file path input on common failures

string text;while (true){    Console.Write("Enter path to a text file: ");    string? path = Console.ReadLine();    try    {        text = File.ReadAllText(path);        break;    }    catch (FileNotFoundException)    {        Console.WriteLine("File not found. Try again.");    }    catch (DirectoryNotFoundException)    {        Console.WriteLine("Folder not found. Try again.");    }    catch (UnauthorizedAccessException)    {        Console.WriteLine("Access denied. Try a different file.");    }    catch (ArgumentException)    {        Console.WriteLine("That path is not valid. Try again.");    }    catch (IOException ex)    {        Console.WriteLine($"I/O error: {ex.Message}");        Console.WriteLine("Try again, or press Ctrl+C to quit.");    }}Console.WriteLine($"You are {age}. File length: {text.Length}");

This keeps the app interactive and resilient. Notice that the try block is small and focused: only the code that can throw the file-related exceptions is inside it.

Handling Errors Locally vs Letting Them Bubble Up

Not every method should catch exceptions. A useful guideline is: catch exceptions at the level where you can do something meaningful.

Handle locally when you can recover or add context

  • You can ask the user to re-enter input.
  • You can choose a fallback file or default value.
  • You can translate a low-level exception into a clearer message.

Example: a method that reads a file for the console UI might catch and return a result that the UI can act on.

static bool TryLoadText(string path, out string text, out string error){    try    {        text = File.ReadAllText(path);        error = "";        return true;    }    catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is ArgumentException)    {        text = "";        error = ex.Message;        return false;    }}

This pattern avoids throwing for expected failures and keeps the calling code simple.

Let exceptions bubble up when you cannot handle them correctly

  • The method has no reasonable recovery action.
  • Catching would force you to guess what to do, potentially hiding bugs.
  • You want a higher layer (like Main) to decide how to present the error.

Example: a lower-level method that parses a configuration file might throw if the file is malformed, and the top-level program decides whether to exit, prompt, or use defaults.

Catch at the top level to control the final user experience

In small console apps, Main is often the right place for a final safety net that prevents a crash message from confusing users. Keep it minimal and avoid swallowing details during development.

static void Main(){    try    {        Run();    }    catch (Exception ex)    {        Console.WriteLine("An unexpected error occurred.");        Console.WriteLine(ex.Message);    }}static void Run(){    // Normal program flow here}

Use this as a last resort. Prefer handling expected problems closer to where they occur (input validation and specific file exceptions), and reserve the top-level catch for truly unexpected failures.

Summary of Improvements Applied to the Original Program

  • Replaced exception-driven input parsing (int.Parse) with validation-driven parsing (int.TryParse).
  • Added domain checks (range validation) to prevent logically invalid values.
  • Wrapped file operations in try/catch and caught specific exceptions to provide actionable feedback.
  • Used retry loops for recoverable errors.
  • Kept try blocks small and caught exceptions only where meaningful handling is possible.

Now answer the exercise about the content:

In a console app that reads an age and then loads a text file, which approach best matches defensive coding for expected failures?

You are right! Congratulations, now go to the next page

You missed! Try again.

Defensive coding validates expected input with TryParse (plus range checks) and handles recoverable file errors with small, focused try/catch blocks that catch specific exceptions and guide the user.

Next chapter

Using NuGet Packages in a Console App

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.