try, catch and finally
Catch specific exception types before general ones, use finally for cleanup that must happen either way, and stop catching Exception.
try { ... } catch (SpecificException ex) { ... } finally { ... }try marks a block of code that might fail. catch gives code to run when it does. finally gives code that runs either way. Together they let a program survive a failure instead of stopping.
Order matters
The runtime checks catch clauses from top to bottom and runs at most one of them. A clause written for a base type also matches every type derived from it, so the specific types go first. catch (Exception) matches every exception, so it has to come last: put anything after it and the compiler reports error CS0160, because that clause can never run.
When you do not need the exception object, you may leave the variable out. catch (FormatException) and catch (FormatException ex) are the same clause; the first simply has nothing to name.
finally
A finally block runs whenever control leaves the try block: normal completion, a return, or an exception travelling outwards. That makes it the place for cleanup that must happen on both the good and the bad path. It runs after the catch block, not before it.
What trips people up
Catching Exception and carrying on is the most common error-handling mistake in C#. It also catches your bugs: a null reference, a bad cast, a case you never thought about. The program then continues with wrong data, and the real fault appears somewhere else hours later. Catch the types you can genuinely do something about. If you only want to record the failure, log it and then write throw; to let it continue.
Example
string[] inputs = ["12", "9999999999", "abc"];
foreach (string input in inputs)
{
try
{
// int.Parse throws FormatException for text that is not a number,
// and OverflowException for a number too large to fit in an int.
int number = int.Parse(input);
Console.WriteLine($"parsed {number}");
}
catch (FormatException)
{
// No variable declared, because nothing here needs the object.
Console.WriteLine($"not a number: {input}");
}
catch (OverflowException)
{
Console.WriteLine($"too big for int: {input}");
}
finally
{
// Runs after the try block, and after any catch block, every time.
Console.WriteLine($"done with {input}");
}
}
// Output:
// parsed 12
// done with 12
// too big for int: 9999999999
// done with 9999999999
// not a number: abc
// done with abcWhen to use it
- Parsing a user-supplied string without the whole program stopping on a typo.
- Resetting a busy flag or releasing a lock on both the success and failure path.
- Adding the file name or request id to a failure before letting it travel on.
More examples
try and finally with no catch at all
A try block does not need a catch. This shape says "I cannot handle a failure here, but this cleanup must happen anyway". Note the ordering in the output: finished prints before 5, because the finally block runs before the method actually returns.
int Divide(int left, int right)
{
Console.WriteLine("start");
try
{
return left / right; // DivideByZeroException when right is 0
}
finally
{
// Runs before the return value reaches the caller, and also before
// the exception leaves this method.
Console.WriteLine("finished");
}
}
try
{
Console.WriteLine(Divide(10, 2));
Console.WriteLine(Divide(10, 0));
}
catch (DivideByZeroException)
{
Console.WriteLine("cannot divide by zero");
}
// Output:
// start
// finished
// 5
// start
// finished
// cannot divide by zeroLog, then let it keep going
This is the only honest reason to catch an exception you cannot fix: to add context on the way past. The method still fails, and the caller still finds out. Swallowing the exception here would turn a failed save into a silent one.
void Save(string path, string contents)
{
try
{
File.WriteAllText(path, contents);
}
catch (IOException ex)
{
// Console.Error writes to the standard error stream, which is where
// a container runtime and most log collectors expect failures.
Console.Error.WriteLine($"could not write {path}: {ex.Message}");
// Rethrow the same exception. Without this line the caller would
// believe the file was saved.
throw;
}
}One clause for two types
The expression after when is an extra condition on the clause: the clause only handles the exception when it is true. This lets a single block handle two unrelated types without widening to every Exception. The or pattern has been available since C# 9.
try
{
int number = int.Parse("abc");
Console.WriteLine(number);
}
// `is FormatException or OverflowException` is shorthand for
// `ex is FormatException || ex is OverflowException`. Both are the same test.
catch (Exception ex) when (ex is FormatException or OverflowException)
{
Console.WriteLine($"bad input: {ex.GetType().Name}");
}
// Output:
// bad input: FormatException
Discussion