Handle run time errors with try, catch and finally, catch specific exception types, throw and rethrow correctly, create custom exceptions and avoid the common mistakes.

Bishal Biswas
Self Learner
What an exception is
Some errors cannot be caught by the compiler. A file might be missing. The user might type letters where a number was expected. A network call might time out. When something like this happens at run time, .NET throws an exception: an object that describes what went wrong.
If nothing catches it, the exception travels up through every calling method until it reaches the top and crashes the program with a stack trace.
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // IndexOutOfRangeException, program stops
try and catch
Wrap risky code in try. If an exception is thrown inside, execution jumps to the matching catch:
try
{
Console.Write("Enter a number: ");
int n = int.Parse(Console.ReadLine() ?? "");
Console.WriteLine($"Double is {n * 2}");
}
catch (FormatException)
{
Console.WriteLine("That was not a number");
}
Console.WriteLine("Program continues");
The lines after the failing one inside try are skipped. The program carries on after the catch block.
Catching specific types
Different problems throw different exception types. Order your catches from most specific to most general. The first match wins:
try
{
string? text = Console.ReadLine();
int n = int.Parse(text!);
int result = 100 / n;
Console.WriteLine(result);
}
catch (FormatException)
{
Console.WriteLine("Not a number");
}
catch (DivideByZeroException)
{
Console.WriteLine("Cannot divide by zero");
}
catch (Exception ex)
{
Console.WriteLine($"Something else went wrong: {ex.Message}");
}
Exception is the base type of every exception, so it catches anything. Put it last, and only if you actually have something useful to do with an unknown error.
The exception object
Give the catch a variable name to inspect it:
catch (Exception ex)
{
Console.WriteLine(ex.Message); // human readable description
Console.WriteLine(ex.GetType()); // the exact type
Console.WriteLine(ex.StackTrace); // where it happened
}
finally
Code in finally runs whether or not an exception occurred. Use it for cleanup that must happen no matter what:
StreamReader? reader = null;
try
{
reader = new StreamReader("data.txt");
Console.WriteLine(reader.ReadLine());
}
catch (FileNotFoundException)
{
Console.WriteLine("File missing");
}
finally
{
reader?.Dispose(); // always closes the file
}
For resources like files, the using statement is shorter and does the same job. It is covered in the file handling topic.
Throwing exceptions
Throw your own when a method receives something it cannot work with:
static decimal Withdraw(decimal balance, decimal amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be positive");
if (amount > balance)
throw new InvalidOperationException("Insufficient funds");
return balance - amount;
}
Pick a built in type that fits. The common ones:
ArgumentExceptionandArgumentNullException: a bad argument.ArgumentOutOfRangeException: an argument outside the allowed range.InvalidOperationException: the object is in the wrong state for this call.NotSupportedExceptionandNotImplementedException: what they say.
Modern .NET has helpers for the most frequent checks:
ArgumentNullException.ThrowIfNull(name);
ArgumentOutOfRangeException.ThrowIfNegative(amount);
Rethrowing
Sometimes you want to log an error and let it continue upwards. Use throw; on its own. Writing throw ex; resets the stack trace and hides where the problem started:
try
{
Process();
}
catch (Exception ex)
{
Console.WriteLine($"Logged: {ex.Message}");
throw; // correct, keeps the original stack trace
// throw ex; // wrong, loses it
}
To wrap an exception in a more meaningful one, pass the original as the inner exception:
catch (IOException ex)
{
throw new DataLoadException("Could not load settings", ex);
}
Exception filters
A when clause catches only when a condition holds:
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("Resource does not exist");
}
Custom exceptions
Create your own when callers need to catch a specific situation. Inherit from Exception and provide the standard constructors:
class InsufficientFundsException : Exception
{
public decimal Shortfall { get; }
public InsufficientFundsException(decimal shortfall)
: base($"Short by {shortfall:C}")
{
Shortfall = shortfall;
}
}
Name it with the Exception suffix and add properties for any data the catcher will need.
Exceptions are for exceptional cases
Throwing and catching is slow compared to an if. Do not use exceptions for normal control flow:
// Bad: expected situation handled as an exception
try { n = int.Parse(input); } catch { n = 0; }
// Good: use the Try pattern
if (!int.TryParse(input, out n)) n = 0;
Rule of thumb: if you expect the failure to happen regularly, return a result or a bool. If it means something has genuinely gone wrong, throw.
Do not swallow exceptions
An empty catch hides bugs:
try { Save(); }
catch { } // the save silently failed and nobody will ever know
At minimum, log the message. Usually, let it propagate to something that can handle it.
Quick recap
- Exceptions are objects thrown at run time when something goes wrong, and uncaught ones crash the program.
trywraps risky code,catchhandles it,finallyalways runs.- Catch specific types first and
Exceptionlast, if at all. throw new ...signals a problem,throw;rethrows without losing the stack trace.- Custom exceptions inherit from
Exceptionand end in the word Exception. - Use
TryParsestyle methods for expected failures and exceptions for real errors.
// Exception handling in C#
class InsufficientFundsException : Exception
{
public decimal Shortfall { get; }
public InsufficientFundsException(decimal shortfall)
: base($"Short by {shortfall}") => Shortfall = shortfall;
}
static decimal Withdraw(decimal balance, decimal amount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount); // built in guard
if (amount > balance)
throw new InsufficientFundsException(amount - balance); // custom exception
return balance - amount;
}
// Specific catches first, general last, finally always runs
string[] inputs = { "50", "abc", "500", "0" };
foreach (string input in inputs)
{
try
{
decimal amount = decimal.Parse(input); // may throw FormatException
decimal left = Withdraw(100, amount);
Console.WriteLine($"Withdrew {amount}, {left} left");
}
catch (FormatException)
{
Console.WriteLine($"'{input}' is not a number");
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"{ex.Message} (shortfall {ex.Shortfall})");
}
catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "amount")
{
Console.WriteLine("Amount must be positive");
}
finally
{
Console.WriteLine("--- attempt finished ---");
}
}
// Rethrow with throw; to keep the stack trace
try
{
try { Withdraw(10, 20); }
catch (Exception ex) { Console.WriteLine($"Logged: {ex.Message}"); throw; }
}
catch (InsufficientFundsException)
{
Console.WriteLine("Handled at the top");
}
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready