C# provides a structured mechanism for handling runtime errors through the try, catch, and finally keywords. This approach enables developers to enticipate, intercept, and respond to exceptional conditions without abrupt program termination.
The .NET runtime defines a rich hierarchy of exception types rooted in the base class System.Exception. While built-in exceptions like DivideByZeroException or NullReferenceException cover common failure modes, custom exceptions can be introduced by inheriting from Exception or one of its standard derivatives.
Basic Syntax and Flow
A minimal exception-handling construct requires only a try block, optionally followed by one or more catch clauses and/or a single finally clause:
try
{
// Code that may throw
}
catch (InvalidOperationException ex)
{
// Handle specific exception
}
catch (Exception ex)
{
// Fallback handler
}
finally
{
// Always executes — cleanup logic here
}
Execution proceeds as follows:
- If no exception occurs,
catchblocks are skipped;finallyruns before control exits the block. - If an exception is thrown, the runtime searches for the first matching
catch(by type compatibility), executes it, then transfers control tofinally. - The
finallyblock always executes — even ifreturn,break, orgotoappears earlier intryorcatch. However, exitingfinallyvia those statements is prohibited.
Practical Example: Division Safeguard
Consider unsafe integer division:
int divisor = 0;
int result = 100 / divisor; // Throws DivideByZeroException
With protection:
int divisor = 0;
int result = 0;
try
{
result = 100 / divisor;
Console.WriteLine("This won't print");
}
catch (DivideByZeroException)
{
Console.WriteLine("Division by zero prevented");
}
finally
{
Console.WriteLine("Cleanup completed");
}
Console.WriteLine($"Final value: {result}");
Output:
Division by zero prevented
Cleanup completed
Final value: 0
Multiple Catch Handlers
When multiple exception types may arise, order mattters: more specific handlers must precede broader ones.
try
{
ProcessInput(userInput);
}
catch (ArgumentNullException argEx)
{
LogError("Null input received", argEx);
}
catch (FormatException fmtEx)
{
LogError("Invalid format detected", fmtEx);
}
catch (Exception genericEx)
{
LogError("Unexpected error occurred", genericEx);
}
Omitting the exception type in catch creates a universal handler:
catch
{
// Catches any exception — use sparingly
}
Throwing and Re-throwing Exceptions
To signal failure programmatically, use throw:
if (value < 0)
throw new ArgumentException("Value must be non-negative");
Within a catch, re-throwing preserves original stack trace:
catch (IOException ioEx)
{
LogWarning("I/O issue encountered");
throw; // Not 'throw ioEx'
}
Custom Exception Implementation
Define domain-specific exceptions for clarity:
public class InsufficientFundsException : Exception
{
public decimal AvailableBalance { get; }
public decimal AttemptedWithdrawal { get; }
public InsufficientFundsException(
decimal balance,
decimal withdrawal)
: base($"Cannot withdraw {withdrawal:C} — balance is {balance:C}")
{
AvailableBalance = balance;
AttemptedWithdrawal = withdrawal;
}
}
Usage:
if (account.Balance < withdrawalAmount)
throw new InsufficientFundsException(account.Balance, withdrawalAmount);
Return Behavior with Finally
A return inside try does not immediately exit — finally executes first, though the return value remains unchanged:
static int GetValue()
{
int val = 42;
try
{
Console.WriteLine("In try");
return val;
}
finally
{
val *= 2;
Console.WriteLine("In finally");
}
}
Output:
In try
In finally
42
The returned value is still 42, because the return expression was evaluated before finally ran.
Best Practicse
- Prefer built-in exceptions over custom ones unless semantics significantly differ.
- Avoid empty
catchblocks — they suppress failures silently. - Use
finallyfor deterministic cleanup (e.g., closing streams, releasing locks). - Never throw
SystemExceptionsubclasses — they indicate runtime-level faults. - When wrapping exceptions, use
throw new CustomException("message", innerException)to preserve diagnostic context.