C# Exceptions
Exceptions are C#’s structured way to report that a program cannot continue normally. Instead of returning a special number or silently ignoring a failure, code can throw an exception and let the caller decide how to recover, log, retry, or stop. Good exception handling makes programs clearer because normal logic stays separate from unusual failure paths.
Overview: How Exceptions Work
An exception is an object that represents a runtime problem. All exception types inherit from System.Exception. Common examples include ArgumentException for invalid arguments, InvalidOperationException for an object being used in the wrong state, FormatException for invalid text conversion, and KeyNotFoundException for missing dictionary keys.
When code uses throw, the CLR immediately stops the current normal flow and begins searching for a matching catch block. This search walks back through the call stack: the current method, then the method that called it, and so on. If a matching handler is found, execution continues inside that catch. If no handler is found, the exception is unhandled and the program terminates after the runtime reports the failure.
A try block marks code that might fail. A catch block handles a particular exception type. A finally block runs whether the try completed successfully or an exception occurred, which makes it useful for cleanup. Many modern C# cleanup scenarios are better handled by using statements, but finally is still the underlying idea: cleanup should happen even on failure.
Exceptions are not just messages. An exception object contains a type, a Message, an optional InnerException, and a stack trace showing the path of method calls that led to the failure. That stack trace is one reason exceptions are valuable for debugging. It answers not only what failed, but where the failure came from.
Exceptions should represent exceptional or invalid conditions, not ordinary control flow. If a user typing invalid input is normal, int.TryParse is usually better than catching FormatException. If a file might not exist, check or use APIs designed for expected absence. Use exceptions when the operation cannot fulfill its contract.
Syntax
try
{
// Code that might throw an exception.
}
catch (InvalidOperationException ex)
{
// Handle the specific failure.
Console.WriteLine(ex.Message);
}
catch (Exception ex) when (ex.Message.Length > 0)
{
// Handle only when the filter is true.
Console.WriteLine(ex.GetType().Name);
}
finally
{
// Cleanup that runs whether the try block succeeded or failed.
}
| Part | Meaning |
|---|---|
try |
Wraps code whose exceptions you want to handle nearby. |
catch (Type ex) |
Handles exceptions assignable to Type. More specific catches must come first. |
when |
Adds an exception filter. The catch runs only when the condition is true. |
finally |
Runs after the try and any matching catch, even when an exception was thrown. |
throw; |
Rethrows the current exception while preserving its original stack trace. |
throw new ... |
Creates and throws a new exception object. |
Examples
Example 1: Catch A Conversion Failure
using System;
class Program
{
static void Main()
{
string input = "twenty";
try
{
int age = int.Parse(input);
Console.WriteLine($"Age next year: {age + 1}");
}
catch (FormatException ex)
{
Console.WriteLine("Could not parse the age.");
Console.WriteLine(ex.GetType().Name);
}
}
}
Output:
Could not parse the age.
FormatException
int.Parse expects text in a valid integer format. The string "twenty" is not valid, so int.Parse throws FormatException. The catch block handles exactly that kind of failure and keeps the program from crashing.
Example 2: Use Finally For Cleanup
using System;
class Program
{
static void Main()
{
bool connectionOpen = false;
try
{
connectionOpen = true;
Console.WriteLine("Opened resource");
throw new InvalidOperationException("The operation failed.");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Handled: {ex.Message}");
}
finally
{
if (connectionOpen)
{
Console.WriteLine("Closed resource");
}
}
}
}
Output:
Opened resource
Handled: The operation failed.
Closed resource
The finally block runs after the matching catch. In real programs the resource might be a file, network connection, database transaction, or lock. This example uses a Boolean flag so the behavior is visible without depending on external resources.
Example 3: Throw And Catch A Custom Exception
using System;
using System.Globalization;
class InsufficientFundsException : Exception
{
public InsufficientFundsException(decimal balance, decimal amount)
: base($"Cannot withdraw {amount.ToString("F2", CultureInfo.InvariantCulture)} from a balance of {balance.ToString("F2", CultureInfo.InvariantCulture)}.")
{
Balance = balance;
Amount = amount;
}
public decimal Balance { get; }
public decimal Amount { get; }
}
class Program
{
static void Withdraw(decimal balance, decimal amount)
{
if (amount > balance)
{
throw new InsufficientFundsException(balance, amount);
}
Console.WriteLine($"Remaining balance: {(balance - amount).ToString("F2", CultureInfo.InvariantCulture)}");
}
static void Main()
{
try
{
Withdraw(50m, 75m);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"Short by: {(ex.Amount - ex.Balance).ToString("F2", CultureInfo.InvariantCulture)}");
}
}
}
Output:
Cannot withdraw 75.00 from a balance of 50.00.
Short by: 25.00
A custom exception is useful when callers need to distinguish a domain-specific failure from other failures. This exception carries structured data through Balance and Amount, not just a message string. The example formats decimals with CultureInfo.InvariantCulture so the output is stable on machines with different regional settings.
Example 4: Exception Filters
using System;
class Program
{
static void Main()
{
string role = "guest";
try
{
throw new UnauthorizedAccessException("Admin area only.");
}
catch (UnauthorizedAccessException ex) when (role == "guest")
{
Console.WriteLine("Guest was redirected.");
Console.WriteLine(ex.Message);
}
}
}
Output:
Guest was redirected.
Admin area only.
The filter after when is checked before the catch body runs. If the condition is false, the runtime keeps searching for another handler. Filters are useful when the exception type is right, but only some states should be handled at this location.
How Exceptions Work Step By Step
- A method detects that it cannot complete its contract, so it executes
throw. - The CLR creates or receives the exception object and records stack trace information as the exception leaves methods.
- Normal execution stops. Local statements after the throw are skipped unless control later returns through a handler.
- The CLR searches for the nearest compatible
catch, checking exception type and anywhenfilter. - Before leaving a protected area, the runtime runs applicable
finallyblocks. - If a handler is found, its catch body executes. The program may recover, log, choose a fallback, or throw again.
- If no handler is found, the exception is unhandled and the process ends.
Catch order matters because C# chooses the first compatible catch in source order. Since every exception derives from Exception, a general catch (Exception) would catch almost everything. Put narrow exception types first and broad fallback handlers last.
Rethrowing matters too. Inside a catch block, throw; preserves the original stack trace. Writing throw ex; throws the same object again but resets the stack trace to the rethrow location, hiding the original failure site. That makes debugging harder.
Common Mistakes
Catching Too Broadly
try
{
int value = int.Parse("abc");
}
catch (Exception)
{
Console.WriteLine("Something went wrong.");
}
This compiles, but it hides the real category of failure. Broad catches are sometimes appropriate at application boundaries, but local code should usually catch the exception types it can actually handle.
using System;
class Program
{
static void Main()
{
try
{
int value = int.Parse("abc");
Console.WriteLine(value);
}
catch (FormatException)
{
Console.WriteLine("Please enter digits only.");
}
}
}
Output:
Please enter digits only.
Putting A General Catch First
try
{
int number = int.Parse("abc");
}
catch (Exception)
{
Console.WriteLine("General handler");
}
catch (FormatException)
{
Console.WriteLine("Format handler");
}
This does not compile because catch (Exception) already catches FormatException. The specific catch is unreachable. Put specific exception types before general ones.
Using Exceptions For Expected Input Checks
using System;
class Program
{
static void Main()
{
string input = "42";
if (int.TryParse(input, out int number))
{
Console.WriteLine(number * 2);
}
else
{
Console.WriteLine("Invalid number");
}
}
}
Output:
84
When invalid input is expected, TryParse communicates the normal branch directly and avoids the cost and noise of throwing and catching. Save exceptions for cases where the operation cannot reasonably produce a normal result.
Best Practices
- Catch the most specific exception type that you can handle correctly.
- Do not catch an exception only to ignore it. If failure is acceptable, document that choice in code through a clear fallback.
- Use
finallyorusingfor cleanup that must happen on both success and failure. - Use
throw;to rethrow from a catch block while preserving the original stack trace. - Include useful messages when throwing exceptions, but do not parse messages in program logic. Use types and properties for decisions.
- Throw
ArgumentNullException,ArgumentException, orArgumentOutOfRangeExceptionwhen method arguments violate requirements. - Create custom exception types only when callers benefit from catching that specific category or reading extra structured data.
- Avoid exceptions for ordinary branching, validation loops, and expected lookup misses when a
Try...pattern exists. - Let unexpected exceptions surface at the correct boundary where they can be logged, reported, or converted into an application-level error.
Practice Exercises
- Write a method named
Dividethat throwsDivideByZeroExceptionwhen the divisor is zero, then catch it inMainand print a friendly message. - Create a program that parses three strings with
int.TryParse. Print the sum of valid numbers and count how many inputs were invalid. - Create a custom
InvalidGradeExceptionfor grades outside0through100. Throw it from a validation method and catch it inMain.
Summary
- Exceptions are objects that represent failures and travel up the call stack until handled.
tryprotects code,catchhandles specific failures, andfinallyperforms cleanup.- Exception type is more important than message text when deciding how to handle a problem.
- Catch specific exceptions before broad exceptions.
- Use
throw;when rethrowing so debugging information is preserved. - Use
TryParseand similar patterns for expected failures that are part of normal program flow. - Custom exceptions are best when they describe a meaningful domain failure and carry useful structured information.
