When something goes wrong at run time (a file is missing, text is not a number, a key is not in a dictionary), .NET throws an exception: an object describing the error. The exception travels up the call stack until a catch block handles it. If nothing does, the program stops and prints the error.
A basic try catch
Wrap the code that can fail in try, and handle the failure in catch:
Output:
42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running
When int.Parse("forty-two") throws, the Console.WriteLine after it in the try block is skipped, the catch (FormatException) block runs, and the loop continues. You can leave out the variable (catch (FormatException)) when you do not need the exception object.
This particular case has a better tool: int.TryParse(input, out int n) returns false instead of throwing. Exceptions are for situations the code does not expect; input that is often invalid is expected, so check it instead of catching.
How an exception travels
An exception thrown deep inside a call chain unwinds every method until it finds a matching handler. Code after the throw in each of those methods does not run.
Output:
OrderTotal finished
5.00
Caught KeyNotFoundException in Main
PriceOf and OrderTotal have no catch, so the exception passes straight through them to Main. Put a handler at the level that knows what to do about the failure, which is often not where it happened.
Catching specific exceptions, in the right order
A catch clause handles its type and every type derived from it. When there are several clauses, the first one that matches wins, so order them from most specific to most general:
Output:
10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException
The last call throws OverflowException, which neither specific clause handles, so the general catch (Exception e) does. Putting catch (Exception) first would make the clauses after it unreachable, and the compiler rejects that with error CS0160.
Exception filters with when
C# 6 added when: a condition that decides whether a catch clause applies. If it is false, the runtime carries on looking for another handler as if the clause did not exist.
Output:
Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401
Filters let you branch on data inside the exception without catching and rethrowing. They are also the clean way to handle two unrelated types identically, as the third clause does. A filter runs before the stack is unwound, so a debugger or crash dump still shows the original state when no filter matches.
finally: code that always runs
A finally block runs when control leaves the try, whether it finished, returned early, or threw. It is where cleanup goes. (One caveat: if nothing anywhere catches the exception, the process can end without running it.)
Output:
Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error
In every case "Close connection" prints before the method's result reaches Main: the finally runs after the return value is computed but before the method actually returns. A try can have a finally with no catch at all, which cleans up while letting the exception continue to the caller.
For objects that implement IDisposable (files, streams, connections), the using statement writes this try/finally for you.
Rethrowing: throw; vs throw e;
Sometimes a catch block logs or records something and then lets the exception continue. How you rethrow decides whether the stack trace survives:
Output:
throw; trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False
throw e; treats the exception as newly thrown from the catch block, so the frames below it, including the method where the error happened, are gone from the trace. Always rethrow with a bare throw;. (The NoInlining attribute is only there because the JIT may merge a method this small into its caller, which would hide it from both traces.)
To add context instead, wrap the exception in a new one and pass the original as the inner exception: throw new ConfigException("Could not start the app", e);. The inner exception keeps its own stack trace, and loggers print the chain. Writing your own exception types is covered in throwing exceptions.
The Exception object
Every exception derives from System.Exception. The members you use most:
| Member | What it holds |
|---|---|
Message | A human readable description |
GetType().Name | The exception type, such as FormatException |
StackTrace | The chain of method calls at the throw point |
InnerException | The exception that caused this one, or null |
ToString() | Type, message, inner exceptions and stack trace together |
Log e.ToString() rather than e.Message when you want to diagnose a failure later: the message alone rarely says where the problem was. Do not show e.ToString() to end users.
Common exception types
| Exception | Typical cause |
|---|---|
NullReferenceException | Calling a member on a null reference |
ArgumentNullException | A method was passed null where it needs a value |
ArgumentOutOfRangeException | An argument or list index is outside the allowed range |
IndexOutOfRangeException | An array index is outside its bounds |
FormatException | int.Parse, DateTime.Parse and friends on text in the wrong format |
InvalidCastException | An explicit cast to a type the object is not |
InvalidOperationException | The object is in the wrong state for the call (empty sequence, modified collection) |
KeyNotFoundException | Reading a missing dictionary key with the indexer |
DivideByZeroException | Integer or decimal division by zero |
OverflowException | A parsed number, checked conversion or checked arithmetic result does not fit the type |
FileNotFoundException, IOException | File system problems |
NullReferenceException, IndexOutOfRangeException and InvalidCastException almost always mean a bug. Fix the code rather than catching them.
Not swallowing exceptions
An empty catch hides every error, including the ones you did not anticipate:
try
{
SaveOrder(order);
}
catch (Exception)
{
// nothing: the order silently was not saved
}
The program keeps running as if the save worked, and the real cause is lost. Guidelines that keep exception handling honest:
- Catch only what you can handle, at the level that can handle it.
- If you catch to log, rethrow with
throw;unless the program can really continue. - Catch
Exceptiononly at the outer edge:Main, a request handler, a worker loop. - Prefer
TryParse,TryGetValueandnullchecks over exceptions for expected cases. Throwing is slow compared to a check, while atryblock that throws nothing costs almost nothing.
Common mistakes
catch (Exception)first. Later clauses become unreachable (CS0160).throw e;to rethrow. It erases the original stack trace; usethrow;.- Empty catch blocks. Errors vanish; at least log and rethrow.
- Using exceptions for control flow. Validate input with
TryParseinstead of catchingFormatException. - Showing
e.Messageof a framework exception to users. The wording differs between .NET versions and is written for developers.
Frequently Asked Questions
How does try catch work in C#?
Code that might fail goes in the try block. If a statement there throws an exception, the rest of the block is skipped and the runtime looks for a catch clause whose type matches the exception, first in the current method and then in each caller. The first matching catch runs, and execution continues after the whole try statement.
Does finally always run in C#?
Almost always: after the try block completes normally, after a catch handles an exception, after a return or break inside the block, and when an exception passes through on its way to a catch further up the call stack. It does not run when the process ends first: a killed process, Environment.FailFast, a StackOverflowException, and on .NET Core and later an exception that nothing catches, which terminates the process before finally blocks run.
How do I catch multiple exceptions in C#?
Write several catch clauses, most specific type first: catch (FileNotFoundException) before catch (IOException) before catch (Exception). The compiler rejects a clause that can never be reached because an earlier one already catches its type. To handle two unrelated types the same way, use a filter: catch (Exception e) when (e is FormatException || e is OverflowException).
What is the difference between throw and throw ex in C#?
Inside a catch, throw; rethrows the current exception with its original stack trace. throw ex; throws the same object but resets the stack trace to the current line, so the method where the error actually happened disappears from the trace. Use throw;, or wrap it: throw new MyException("context", ex);.
What is an exception filter in C#?
A when clause after a catch (C# 6 and later): catch (HttpRequestException e) when (e.Message.Contains("404")). The catch block runs only if the condition is true; otherwise the exception keeps looking for another handler as if the clause were not there, and its stack is not unwound.
Should I catch Exception in C#?
Only at the edges of a program: the top of Main, a request handler, or a background loop, where the job is to log the error and keep going or exit cleanly. Deep in the code, catch the specific types you can actually handle, and let everything else propagate.