Exception Handling
Things go wrong: a user types “abc” for their age, a file is missing, a server is down. Exceptions let your program notice the problem and respond instead of crashing.
On this page · 6 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
In simple words#
try/catch is a safety net under a trapeze artist. If something goes wrong (an exception), the net catches it and the show continues gracefully instead of crashing.
- Exception
- An error that happens while the program runs.
- try / catch
- Run risky code and handle the error.
- finally
- Code that always runs, error or not.
Learning objectives#
- Catch exceptions with try and catch
- Catch specific exception types in the right order
- Run clean-up code with finally
- Throw your own and custom exceptions
Explanation#
Code that might fail goes in try { }. If an exception is thrown, C# jumps to the first matching catch block.
Catch specific types first (FormatException, ArgumentOutOfRangeException) and general ones (Exception) last. The first matching catch wins.
finally { } always runs — whether the try succeeded, failed or returned — so it is the place to close files or log the end of an operation. using does this automatically for IDisposable objects.
throw new ArgumentException("...") signals a problem. Create a custom exception by inheriting from Exception when callers need to handle a specific business error.
Exceptions are for unexpected situations. For expected failures, such as invalid user input, prefer the Try pattern: int.TryParse(text, out int value).
Syntax#
try
{
// risky code
}
catch (FormatException ex) // specific first
{
}
catch (Exception ex) // general last
{
}
finally
{
// always runs
}
Code example: Bad input and business errors#
Console.WriteLine(ParseAge("25"));
Console.WriteLine(ParseAge("abc"));
Console.WriteLine(ParseAge("-3"));
try
{
Withdraw(balance: 100m, amount: 500m);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine(quot;Error: {ex.Message} (short by {ex.Shortfall})");
}
finally
{
Console.WriteLine("Transaction log closed");
}
Console.WriteLine(int.TryParse("42", out int n) ? quot;Parsed {n}" : "Not a number");
static string ParseAge(string input)
{
try
{
int age = int.Parse(input);
if (age < 0) throw new ArgumentOutOfRangeException(nameof(input), "Age cannot be negative");
return quot;Age: {age}";
}
catch (FormatException)
{
return quot;'{input}' is not a number";
}
catch (ArgumentOutOfRangeException ex)
{
return ex.Message;
}
}
static void Withdraw(decimal balance, decimal amount)
{
if (amount > balance) throw new InsufficientFundsException(amount - balance);
}
class InsufficientFundsException(decimal shortfall) : Exception("Insufficient funds")
{
public decimal Shortfall => shortfall;
}
Age: 25 'abc' is not a number Age cannot be negative (Parameter 'input') Error: Insufficient funds (short by 400) Transaction log closed Parsed 42
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- int.Parse("abc") throws FormatException, which the first catch handles.
- A negative age is valid number syntax, so the code throws ArgumentOutOfRangeException itself; .NET adds “(Parameter 'input')” to the message.
- InsufficientFundsException is a custom exception carrying extra data (Shortfall).
- finally runs after the catch, so the log is always closed.
- int.TryParse avoids exceptions entirely when bad input is expected.
Real-world use case#
An ASP.NET Core API throws NotFoundException from the service layer; a global exception handler turns it into a 404 response, and any unexpected exception into a 500 with the details logged — never shown to the user.
Common mistakes#
catch (Exception) { } hides bugs forever. At minimum log the exception; usually let it bubble up.
Re-throwing with throw ex; resets the stack trace, so you lose where the error really happened. Use throw; inside a catch.
Throwing on every invalid form field is slow and noisy. Validate first or use TryParse.
Best practices#
- Catch only exceptions you can actually handle
- Order catches from specific to general
- Use using / await using for anything IDisposable
- Handle unexpected exceptions in one global place in web apps
Remember this#
try = attempt. catch = handle. finally = always clean up.
Specific catches first, Exception last.
Expected problem → TryParse. Unexpected → exception.
Practice exercise#
Your task: Write SafeDivide(int a, int b) that returns a / b, but returns 0 and prints "Cannot divide by zero" when b is 0, by catching DivideByZeroException.
Need a hint?
Show solution
Console.WriteLine(SafeDivide(10, 2)); // 5
Console.WriteLine(SafeDivide(10, 0)); // Cannot divide by zero, then 0
static int SafeDivide(int a, int b)
{
try
{
return a / b;
}
catch (DivideByZeroException)
{
Console.WriteLine("Cannot divide by zero");
return 0;
}
}
Mini quiz#
When does a finally block run?
Want more? Practise all C# MCQs · C# interview questions
My notes#
Saved in this browser.