LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 2018 min read

Exception Handling

Learn try, catch, finally, and how to handle runtime errors gracefully in C#.

What is an Exception?

An exception is an error that occurs while a program is running — like dividing by zero, accessing an invalid array index, or converting text that isn't actually a number. Without handling, an exception propagates upward, unwinding the call stack, and if nothing catches it, crashes the entire program with an unhandled exception message.

try / catch

A try block wraps code that might throw an exception; a matching catch block runs if one actually occurs, letting your program recover instead of crashing.

try
{
int result = 10 / int.Parse("0");
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"Something went wrong: {ex.Message}");
}
Output

Click Run to see what this code prints.

The Exception Hierarchy

Every exception type in .NET ultimately derives from the base System.Exception class — this is why catch (Exception ex) can catch literally anything, and why more specific exception types (like DivideByZeroException) can be caught individually when you want more targeted handling.

Exception TypeCommon Cause
DivideByZeroExceptionInteger division by zero
IndexOutOfRangeExceptionAccessing an array index that doesn't exist
NullReferenceExceptionCalling a method or accessing a property on a null reference
FormatExceptionA string couldn't be parsed into the requested type (e.g. int.Parse("abc"))
ArgumentExceptionAn invalid argument was passed to a method
InvalidOperationExceptionA method call is invalid given the object's current state

Catching Specific Exceptions

It's better practice to catch specific exception types rather than a generic `Exception`, so you handle each failure mode appropriately — a missing file and an invalid password should almost certainly be handled differently, not lumped into one generic message.

try
{
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]);
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("That index doesn't exist in the array.");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
Order Matters: Most Specific Catch Block First

Because catch blocks are checked in order, a generic catch (Exception) placed BEFORE a more specific catch (IndexOutOfRangeException) would catch everything first, making the specific block unreachable — the compiler actually flags this as an error, requiring specific exception types to appear before more general ones.

finally

Code in a `finally` block always runs, whether or not an exception occurred, and even if the try or catch block returns early — commonly used to release resources like file handles, network connections, or database connections, guaranteeing cleanup regardless of how the block exits.

try
{
Console.WriteLine("Trying...");
throw new Exception("Oops");
}
catch (Exception ex)
{
Console.WriteLine($"Caught: {ex.Message}");
}
finally
{
Console.WriteLine("Cleanup always runs.");
}
Output

Click Run to see what this code prints.

The using Statement (Automatic Cleanup)

For objects that hold an unmanaged resource (like a file handle) and implement IDisposable, a using statement automatically calls Dispose() when the block ends — equivalent to, but far less error-prone than, manually writing a try/finally that calls Dispose() yourself.

using (var file = new StreamReader("data.txt"))
{
string content = file.ReadToEnd();
Console.WriteLine(content);
} // file.Dispose() is called automatically here, even if an exception occurred above

Modern C# (8.0+) also supports a using declaration, without braces, that disposes the resource at the end of the enclosing scope.

using var file = new StreamReader("data.txt");
string content = file.ReadToEnd();
Console.WriteLine(content);
// file is automatically disposed at the end of this method

Throwing Your Own Exceptions

The throw keyword raises an exception explicitly — useful for validating input and refusing to proceed when something is genuinely wrong.

static double Divide(double a, double b)
{
if (b == 0)
{
throw new ArgumentException("Cannot divide by zero.");
}
return a / b;
}
try
{
Divide(10, 0);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}

Rethrowing Correctly

Sometimes a catch block needs to do something (like logging) and then let the exception continue propagating upward. A bare throw; preserves the original exception's full stack trace, while throw ex; resets it — a subtle but important distinction for debugging.

Preserves the Original Stack Trace

csharp
catch (Exception ex)
{
LogError(ex);
throw; // rethrows the SAME exception, stack trace intact
}

Resets the Stack Trace

csharp
catch (Exception ex)
{
LogError(ex);
throw ex; // technically works, but the ORIGINAL throw location is lost
}

Common Beginner Mistakes

Using exceptions for expected, normal control flow

Exceptions carry real performance overhead and are meant for genuinely exceptional situations — use TryParse-style patterns or upfront validation for routine, expected failure cases like invalid user input.

Catching Exception and silently swallowing it

catch (Exception ex) { } with an empty body hides real bugs — at minimum, log the exception, even if you've decided the program can safely continue.

Using throw ex instead of throw when rethrowing

This resets the exception's stack trace, making it much harder to trace back to where the error actually originated.

Forgetting to dispose of unmanaged resources

A file, network connection, or database connection left open can leak resources over time — always use a using statement/declaration for any IDisposable object.

FAQs

No — exceptions are relatively expensive and meant for exceptional situations. Use TryParse-style patterns or upfront validation for expected failure cases, reserving exceptions for genuinely unexpected problems.

Yes — catch (IOException or InvalidOperationException ex) using the "or" pattern (C# 9+) combines several types into one handler when they should be treated identically.

Message gives just the human-readable error description; ToString() gives that plus the exception type name and full stack trace — useful for logging, but too verbose to show directly to an end user.

Yes — finally always runs before control actually leaves the method, even if try or catch contains a return, making it reliable for cleanup regardless of how the block exits.

Key Takeaways

  • try/catch prevents an exception from crashing your program, and every exception ultimately derives from System.Exception.
  • Catch specific exception types (in order from most to least specific) when possible, for clearer, more targeted handling.
  • finally always runs, regardless of whether an exception occurred or the block returned early — ideal for cleanup code.
  • Prefer using statements/declarations over manual try/finally for disposing of IDisposable resources.
  • Use bare throw; (not throw ex;) when rethrowing, to preserve the original stack trace for debugging.

Summary

Exception handling — try/catch/finally, the using statement, and careful rethrowing — is how production C# code fails gracefully instead of crashing outright. Next, you'll begin the object-oriented programming section of this course with classes and objects.

Next Lesson →

Classes & Objects