What is exception handling and how does it work in C#?
Quick Answer
Exception handling allows graceful handling of runtime errors using try-catch-finally blocks. When an error occurs, an exception object is created and the runtime searches for appropriate handlers. Use specific exception types, avoid catching and ignoring exceptions, use 'throw' instead of 'throw ex' to preserve stack trace, and include cleanup code in finally blocks.
Detailed Answer
Exception Handling is a mechanism in C# that allows you to handle runtime errors gracefully, preventing your application from crashing and providing meaningful error messages to users.
How Exception Handling Works:
- When an error occurs, an exception object is created
- The runtime searches for an appropriate exception handler
- If found, the handler executes and the program continues
- If not found, the program terminates with an unhandled exception
Basic Exception Handling Structure:
try
{
// Code that might throw an exception
int result = Divide(10, 0);
}
catch (DivideByZeroException ex)
{
// Handle specific exception
Console.WriteLine($"Error: {ex.Message}");
}
catch (Exception ex)
{
// Handle any other exception
Console.WriteLine($"Unexpected error: {ex.Message}");
}
finally
{
// Always executes (cleanup code)
Console.WriteLine("Cleanup code here");
}
Exception Hierarchy:
System.Object
└── System.Exception
├── System.SystemException
│ ├── ArgumentException
│ ├── NullReferenceException
│ ├── IndexOutOfRangeException
│ └── DivideByZeroException
└── System.ApplicationException
└── Custom exceptions
Key Differences: throw vs throw ex vs throw new
1. throw (rethrow):
try
{
// Some operation
}
catch (Exception ex)
{
// Log the exception
LogError(ex);
// Rethrow the original exception (preserves stack trace)
throw; // GOOD - keeps original stack trace
}
2. throw ex (lose stack trace):
try
{
// Some operation
}
catch (Exception ex)
{
// Log the exception
LogError(ex);
// Rethrow but lose original stack trace
throw ex; // BAD - loses original stack trace
}
3. throw new (new exception):
try
{
// Some operation
}
catch (Exception ex)
{
// Create new exception with original as inner exception
throw new CustomException("Something went wrong", ex); // GOOD
}
When to Use Each:
- try-catch: Handle exceptions you can recover from
- try-finally: Ensure cleanup code always runs
- try-catch-finally: Handle exceptions AND ensure cleanup
Custom Exceptions:
// Custom exception class
public class InsufficientFundsException : Exception
{
public decimal CurrentBalance { get; }
public decimal RequiredAmount { get; }
public InsufficientFundsException(decimal currentBalance, decimal requiredAmount)
: base($"Insufficient funds. Current: {currentBalance:C}, Required: {requiredAmount:C}")
{
CurrentBalance = currentBalance;
RequiredAmount = requiredAmount;
}
public InsufficientFundsException(string message, Exception innerException)
: base(message, innerException)
{
}
}
// Usage
public void Withdraw(decimal amount)
{
if (amount > Balance)
{
throw new InsufficientFundsException(Balance, amount);
}
Balance -= amount;
}
Best Practices:
- Catch specific exceptions when possible
- Don't catch and ignore exceptions silently
- Use
throwinstead ofthrow exto preserve stack trace - Include inner exceptions when creating new exceptions
- Log exceptions before rethrowing
- Use finally blocks for cleanup code
- Don't throw exceptions for normal program flow