Exception-Handling Statements
Exception-handling statements separate normal program flow from failure flow. They let you describe what should happen when an operation cannot complete as expected.
This matters because real programs work with uncertain input and unreliable environments:
- text may fail to parse
- files may not exist
- network calls may fail
- objects may be in an invalid state
If all error logic is mixed directly into the main path, code quickly becomes harder to read. Exception handling provides a structured alternative.
The main exception-handling statements
trycatchfinallythrow
Exception flow at a glance
flowchart TD
A["Enter try block"] --> B["Run protected code"]
B --> C{"Exception thrown?"}
C -- No --> D["Skip catch blocks"]
C -- Yes --> E{"Matching catch available?"}
E -- Yes --> F["Run matching catch block"]
E -- No --> G["Propagate exception outward"]
D --> H{"finally present?"}
F --> H
G --> H
H -- Yes --> I["Run finally block"]
H -- No --> J["Continue or terminate"]
I --> JThis diagram captures the most important idea: exceptions do not follow normal statement-by-statement flow. They interrupt it.
try and catch
Use try for the code that might fail and catch for the code that handles a matching exception.
try
{
int number = int.Parse("not-a-number");
}
catch (FormatException ex)
{
Console.WriteLine($"Input was invalid: {ex.Message}");
}Here is the flow:
- The program enters the
tryblock. int.Parsethrows aFormatException.- Normal execution of the
tryblock stops immediately. - The matching
catchblock runs.
Any statements after the point of failure inside the try block are skipped.
Multiple catch blocks
Different failures often need different responses.
try
{
string text = File.ReadAllText("settings.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine("The settings file was not found.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("The file exists, but access is denied.");
}The runtime chooses the first compatible catch block. That means order matters, especially when exceptions are related by inheritance.
finally
Use finally for cleanup work that should happen whether the protected code succeeds or fails.
StreamReader? reader = null;
try
{
reader = new StreamReader("data.txt");
Console.WriteLine(reader.ReadLine());
}
catch (IOException ex)
{
Console.WriteLine(ex.Message);
}
finally
{
reader?.Dispose();
}finally is about guaranteed cleanup. It is not mainly for error messages. It is for things the program must still do on the way out.
throw
throw creates or rethrows an exception.
static decimal CalculateUnitPrice(decimal total, int quantity)
{
if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(quantity), "Quantity must be greater than zero.");
}
return total / quantity;
}This is how code signals that execution cannot continue normally.
Rethrowing exceptions correctly
If you catch an exception only to log it and pass it along, prefer throw; instead of throw ex;.
catch (Exception)
{
throw;
}throw; preserves the original stack trace more accurately.
Exceptions versus normal validation
One of the most important design habits is knowing when not to use exceptions.
Use regular conditions for expected situations:
- checking whether input is empty
- using
TryParsefor user-entered text - testing whether a collection has items
Use exceptions for abnormal or failed operations:
- a required file is missing
- an invalid state makes the operation impossible
- an argument violates a method contract
For example, this is often better than relying on exceptions for user input:
if (int.TryParse("42", out int result))
{
Console.WriteLine(result);
}
else
{
Console.WriteLine("Input was not a valid integer.");
}A worked example
Imagine a method that reads a quantity from text, rejects invalid input, and always logs that processing finished.
string input = "12";
try
{
int quantity = int.Parse(input);
if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(quantity), "Quantity must be positive.");
}
Console.WriteLine($"Quantity accepted: {quantity}");
}
catch (FormatException)
{
Console.WriteLine("The quantity must be a whole number.");
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("Processing finished.");
}This example shows three different roles:
trycontains the risky workcatchhandles specific failuresfinallyruns cleanup or finalization logic
Common mistakes
- Catching
Exceptiontoo broadly when only a specific exception should be handled. - Using exceptions for ordinary control flow that should be handled with conditions.
- Swallowing exceptions silently without logging, fixing, or rethrowing them.
- Putting too much code in one
tryblock, which makes it harder to see what really might fail.
Summary
Exception-handling statements let you describe failure paths without mixing them into every normal statement.
The main ideas are:
tryprotects code that may failcatchhandles specific exceptionsfinallyruns cleanup logicthrowsignals a failure explicitly
Good exception handling is specific, deliberate, and focused on real failure scenarios rather than ordinary branching.
Practice
Write a small example that uses int.Parse inside a try block and handles FormatException.
As a second exercise, write a method that throws ArgumentException when a required string parameter is empty or whitespace.