Exceptions
By the end of this lesson you'll be able to stop your C# programs crashing on bad input — catching errors with try/catch/finally , handling specific problems precisely, throwing your own exceptions, and knowing when to skip exceptions entirely with TryParse .
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 Real-World Analogy
An exception is a circuit breaker in your house. When something goes wrong — a short circuit, too much load — the breaker trips instead of letting the whole house burn down. The try block is the wiring that might fault; the catch block is the breaker that trips and lets you respond calmly; the finally block is the safety routine that runs whether or not anything tripped. Without it, one bad value (a user typing "abc" where you expected a number) takes the entire program down.
📊 Common Exception Types
Exception
Thrown when…
Typical cause
NullReferenceException
You use a variable that is null
obj.Name when obj is null
FormatException
Text can't convert to a number
int.Parse("abc")
IndexOutOfRangeException
You read past the end of an array
arr[10] in a size-3 array
DivideByZeroException
You divide an int by 0
10 / 0
InvalidOperationException
An object is in the wrong state
list.First() on empty list
ArgumentException
A method gets a bad argument
you throw it on invalid input
Every one of these inherits from Exception , which is why a single catch (Exception ex) can catch them all — but, as you'll see, catching the specific type is usually better.
1. try / catch / finally
An exception is C#'s way of saying "I can't do this" at runtime — like parsing the text "abc" as a number. Left alone, it crashes your program. You put the risky code inside a try block; if it throws, C# jumps to a matching catch block instead of crashing. The optional finally block runs no matter what — perfect for cleanup. Read this worked example, run it, then you'll write your own.
Your turn. The program below would crash because "oops" isn't a number. Wrap it in a try/catch so it fails gracefully — fill in the blanks marked ___ using the hints.
2. Catching Specific Exceptions
Catching the broad Exception works, but it treats every problem the same. Usually you want to react differently to different errors — a bad index isn't the same as bad text. So you list several catch blocks, each for a specific type. The rule that trips everyone up: specific catches must come before the general one . C# checks them top to bottom and stops at the first match, so if catch (Exception) came first it would swallow everything and the compiler rejects the unreachable ones below it.
Now you try. Dividing an int by zero throws a specific exception. Catch exactly that type, then add a finally block that always runs.
3. Throwing & Re-throwing
You don't only catch exceptions — you can throw them too. When a method is handed input it can't work with, the cleanest response is throw new ArgumentException("...") so the caller knows immediately. If you catch an exception just to log it but can't fully handle it, re-throw with a bare throw; . Crucially, throw; keeps the original stack trace (the breadcrumb trail showing where the error really started); writing throw ex; instead resets it and hides the true source.
🔎 Deep Dive: Custom Exceptions
When none of the built-in types describe your problem well, make your own. A custom exception is just a class that inherits from Exception . The big win is that it can carry extra data — here, exactly how much money is missing — so the catch block can give a genuinely helpful message instead of a generic one.
Name custom exceptions ending in Exception by convention, and only create them when a built-in type genuinely doesn't fit.
4. When NOT to Use Exceptions
Exceptions are for the unexpected . A user typing letters into an age box is completely expected, so reaching for try/catch there is the wrong tool — it's slower and noisier. Use TryParse , which returns true / false instead of throwing. Separately, when you open something that must be closed (files, streams, connections), wrap it in a using block: it calls Dispose() for you automatically, even if an exception is thrown — the tidy version of a try/finally cleanup.
Common Errors (and the fix)
- "CS0160: A previous catch clause already catches all exceptions": you put catch (Exception) before a more specific catch, making the lower one unreachable. Order specific → general.
- Swallowing exceptions silently: an empty catch { } hides the bug — the program limps on in a broken state. At minimum log ex.Message ; only catch what you can actually handle.
- Catching Exception too broadly: a blanket catch also hides errors you never meant to handle (typos, null bugs). Catch the specific type, and let truly unexpected ones surface.
- Re-throwing with throw ex; : this resets the stack trace so you lose where the error began. Use a bare throw; to preserve it.
- Using exceptions for normal control flow: looping with try/catch to test if input parses is slow and unreadable. Use int.TryParse(...) — that's what it's for.
Pro Tips
- 💡 Catch specific, throw specific. The more precise the type, the more precise the fix — and the fewer real bugs you accidentally hide.
- 💡 Re-throw with throw; , never throw ex; — keep the original stack trace intact.
- 💡 Prefer TryParse over Parse for anything a user typed; it's far faster than throwing and catching.
- 💡 Use using for anything disposable (files, streams) so cleanup happens even on error.
- 💡 finally always runs — even if you return from inside the try . Put guaranteed cleanup there.
📋 Quick Reference
Task
Code
Notes
Catch any error
catch (Exception ex)
Put it last
Read the message
ex.Message
Human-readable text
Always run
finally { ... }
Cleanup
Throw an error
throw new ArgumentException("…")
Signal bad input
Re-throw
throw;
Keeps stack trace
Parse safely
int.TryParse(s, out int n)
true / false
Auto-cleanup
using (var f = …) { … }
Calls Dispose()
Frequently Asked Questions
Q: Should I just wrap my whole program in one big try/catch?
No. A giant catch hides where the error came from and tempts you to ignore it. Catch close to the risky operation, catch the specific type, and only handle what you can actually recover from.
Q: When do I use TryParse instead of try/catch ?
Whenever failure is expected — typically user input or file content. Throwing and catching is slow; TryParse just returns false . Save exceptions for genuinely unexpected situations.
Q: What's the difference between throw; and throw ex; ?
throw; re-throws the current exception and preserves the original stack trace (where it really started). throw ex; throws it again but resets the trace to this line, hiding the source. Almost always use throw; .
Q: Does finally run even if there's a return in the try ?
Yes. finally runs whether the try finishes normally, throws, or returns early. That guarantee is exactly why it's the right place for cleanup.
Mini-Challenge: Safe Divider
No blanks this time — just a brief and an outline. Combine everything: parse user input safely , then divide inside a try/catch so neither bad text nor a zero can crash you. Run it with "4" , "0" , and "abc" to check all three paths.
🎉 Lesson Complete
- ✅ try holds risky code; catch handles the error; finally always runs
- ✅ Know the common types: NullReference , Format , IndexOutOfRange , DivideByZero , InvalidOperation
- ✅ Order catches specific → general (general last) or you'll hit CS0160
- ✅ throw to signal bad input; re-throw with throw; to keep the stack trace
- ✅ Custom exceptions inherit from Exception and can carry extra context
- ✅ Use TryParse for expected failures and using for guaranteed cleanup
- ✅ Next lesson: LINQ — query, filter, and transform collections in one clean expression
Practice quiz
What goes inside a try block?
- Cleanup code that must always run
- The catch handler
- Code that might throw an exception
- The exception's message
Answer: Code that might throw an exception. A try block holds the risky code that might throw; if it throws, control jumps to a matching catch.
When does a finally block run?
- Always — whether or not an exception occurred
- Only when an exception is thrown
- Only when no exception is thrown
- Only if you call it explicitly
Answer: Always — whether or not an exception occurred. finally always runs — crash or no crash, even after a return in the try — which makes it ideal for cleanup.
How must you order multiple catch blocks?
- General before specific
- Alphabetically
- Order doesn't matter
- Specific before general
Answer: Specific before general. Specific catches must come before the general catch (Exception); C# checks top to bottom and a general catch first would make the rest unreachable (CS0160).
What does ex.Message give you?
- The line number
- A human-readable description of what went wrong
- The exception type name only
- The full stack trace
Answer: A human-readable description of what went wrong. ex.Message is the human-readable description of the error carried on the exception object.
Which exception is thrown by ?
- FormatException
- IndexOutOfRangeException
- DivideByZeroException
- NullReferenceException
Answer: FormatException. Parsing text that isn't a valid number throws FormatException.
Which exception is thrown by dividing an int by zero ( )?
- ArgumentException
- FormatException
- DivideByZeroException
- InvalidOperationException
Answer: DivideByZeroException. Dividing an int by zero throws DivideByZeroException.
What is the difference between and in a catch block?
- They are identical
- throw; preserves the original stack trace; throw ex; resets it
- throw ex; preserves the stack trace; throw; resets it
- throw; rethrows a new exception type
Answer: throw; preserves the original stack trace; throw ex; resets it. A bare throw; re-throws the current exception and keeps the original stack trace; throw ex; resets the trace to that line, hiding the source.
How do you create a custom exception?
- Implement IException
A custom exception is just a class that inherits from Exception, and it can carry extra data for the caller.
For expected failures like bad user input, what should you use instead of try/catch?
- A bigger try block
- int.TryParse (returns true/false)
- throw new Exception
- A finally block
Answer: int.TryParse (returns true/false). TryParse returns true/false instead of throwing — far faster and cleaner than try/catch for expected failures.
What does a block guarantee for a disposable resource?
- It catches all exceptions
- It retries the operation
- It calls Dispose() automatically, even if an exception is thrown
- It logs the error
Answer: It calls Dispose() automatically, even if an exception is thrown. A using block calls Dispose() for you when the block ends, even on error — the tidy alternative to a try/finally that closes resources.
Continue this course
- Previous: Collections
- Next: LINQ