In processes like file operations or network communication, a single operation can have multiple different error causes. For example, when reading a file, the guidance for the user and the system’s recovery response differ significantly between “File not found” and “No access permission.”
In C#, by writing multiple catch blocks in the try-catch syntax, you can implement fine-grained error handling based on the type of exception that occurred. In this article, I will explain how to appropriately properly categorize and handle exceptions using a configuration file reading process as an example.
Distributing Exceptions with Multiple Catch Blocks
An important principle in exception handling is “Write specific (detailed) exceptions first, and general exceptions last.”
C# exception classes have an inheritance relationship. If you catch a parent class (e.g., Exception), it includes all of its child class exceptions. Therefore, if you write the parent class first, the program will never reach the specific exception handling (child class catch blocks) written afterwards (this will result in a compilation error).
Practical Code Example: Reading a Configuration File
The following code processes reading an application configuration file. It distinguishes between four cases: “File not found,” “Directory not found,” “No permission,” and “Other IO errors.”
using System;
using System.IO;
namespace ConfigurationManager
{
class Program
{
static void Main()
{
// Scenario:
// Read an important configuration file (settings.xml) at application startup.
// We need to change the message to the user depending on the error that occurs.
string filePath = @"Configs\settings.xml";
try
{
// Attempt to read file
// Various IO errors can occur here.
string content = File.ReadAllText(filePath);
Console.WriteLine("Successfully read the configuration file.");
Console.WriteLine($"Content: {content}");
}
// 1. Most specific exception: The file itself does not exist
catch (FileNotFoundException ex)
{
Console.WriteLine("[Error] Configuration file not found.");
Console.WriteLine($"Filename: {ex.FileName}");
// Recovery idea: Write code to generate default settings here
}
// 2. Specific exception: The directory in the path does not exist
catch (DirectoryNotFoundException)
{
Console.WriteLine("[Error] Configuration folder (Configs) does not exist.");
// Recovery idea: Write code to create the folder here
}
// 3. Specific exception: No permission to access the file
catch (UnauthorizedAccessException)
{
Console.WriteLine("[Error] No permission to access the file. Please run as administrator.");
}
// 4. Slightly general exception: Other IO errors (Disk full, file in use, etc.)
catch (IOException ex)
{
Console.WriteLine($"[Error] An IO error occurred while reading the file: {ex.Message}");
}
// 5. Most general exception: All unexpected errors
catch (Exception ex)
{
Console.WriteLine($"[Critical Error] An unexpected problem occurred: {ex.Message}");
}
}
}
}
Example Execution Result (When file is missing)
[Error] Configuration file not found.
Filename: settings.xml
Technical Points and Precautions
1. Inheritance Relationship and Order of Description
Some of the exception classes appearing in the code above have the following inheritance relationship:
ObjectException(Base of all exceptions)SystemExceptionIOExceptionFileNotFoundExceptionDirectoryNotFoundException
If you write catch (IOException) above catch (FileNotFoundException), “File not found” errors will also be caught by the IOException block, preventing individual processing. While the compiler may issue a warning or error about unreachable code, you should remember the basic rule: “Derived classes (children) go on top, Base classes (parents) go on the bottom.”
2. Using Exception Filters (when clause)
In C# 6.0 and later, you can use the when keyword to filter whether to catch an exception based on specific conditions (such as property values), even for the same exception type.
catch (IOException ex) when (ex.Message.Contains("Disk full"))
{
Console.WriteLine("Disk space is insufficient.");
}
This enables fine-grained control that cannot be handled by type branching alone.
Summary
Properly catching and classifying multiple exceptions is essential for creating robust applications. If you simply process all errors together with Exception, users will not understand why the operation failed, and appropriate action cannot be taken. Use this pattern to clarify the cause of errors and provide recovery processing tailored to the situation.
