Exceptions in C#

When something goes wrong at run time, C# throws an exception, and the program crashes unless code catches it. Prevent what you can check for; catch what you can't, most specific type first; and throw your own when a method can't do what it's asked.

Apprentice · C# · Updated

After this lesson you can

  • Catch an exception with try and catch, catching specific exception types before general ones
  • Prevent errors you can check for with if or TryParse, and keep try and catch for those you can't
  • Throw an exception when a method is asked to do something it can't

Key takeaways

  • An exception that no code catches crashes the program. Put risky code in a try, and handle the failure in a catch.
  • C# runs the first catch that matches, so put FileNotFoundException before IOException. Catching Exception itself is a last resort.
  • If you can check for a problem with an if or TryParse, check instead of catching; keep try and catch for what you can't check, such as files.
  • A finally block runs after the try, whether an exception was thrown or not.
  • When your method is asked to do something it can't, throw an exception such as ArgumentException instead of carrying on with bad data.

Some problems can't be seen until a program runs. A user types abc where you asked for a number. A file you need has been deleted. Another program has the file open. These are run-time errors, and the compiler can't warn you about them.

When one happens, C# creates an exception: an object that describes what went wrong, such as a FormatException or a FileNotFoundException. Then it throws it. If no code catches the exception, the program crashes.

An exception nobody catches

int quantity = int.Parse("abc");
Console.WriteLine(quot;You ordered {quantity}");
Unhandled exception. System.FormatException: The input string 'abc' was not in a correct format.

The second line never runs. The exception stops the program in its tracks, and C# prints the exception's type, its message, and a list of the methods it passed through (the stack trace), which tells you where to look.

You've seen this before: an index past the end of an array, Dequeue on an empty queue. Each was an exception.

try and catch

To handle an exception instead of crashing, put the risky code in a try block, and say what to do if it fails in a catch block:

string input = "abc";

try
{
    int quantity = int.Parse(input);
    Console.WriteLine(quot;You ordered {quantity}");
}
catch (FormatException ex)
{
    Console.WriteLine(quot;Sorry: {ex.Message}");
}

Console.WriteLine("The program carries on");
Sorry: The input string 'abc' was not in a correct format.
The program carries on

When int.Parse throws, the rest of the try block is skipped, and C# jumps to the catch block whose type matches. ex is the exception object, and ex.Message describes what went wrong. After the catch, the program carries on as normal.

If nothing goes wrong, the catch block is skipped entirely.

Catching specific types

Catch the exceptions you expect and know what to do about. Reading a file can fail in different ways, and each deserves its own message:

try
{
    string text = File.ReadAllText("student_grades.csv");
    Console.WriteLine(text);
}
catch (FileNotFoundException)
{
    Console.WriteLine("There's no student_grades.csv. Check the file name.");
}
catch (IOException ex)
{
    Console.WriteLine(quot;The file couldn't be read: {ex.Message}");
}
There's no student_grades.csv. Check the file name.

C# tries the catch blocks in order, and runs the first one that matches. A FileNotFoundException is an IOException (exceptions use inheritance too), so the specific one must come first. Here the IOException block handles everything else that can go wrong with a file, such as another program having it locked. (When a catch block doesn't use the exception, you can leave out the variable name, as the first one does.)

At the very top of the family is Exception, which matches every exception. Put a more specific catch after it, and C# refuses, since the specific one could never run:

try
{
    string text = File.ReadAllText("student_grades.csv");
}
catch (Exception)
{
    Console.WriteLine("Something went wrong");
}
catch (FileNotFoundException)
{
    Console.WriteLine("There's no student_grades.csv");
}
error CS0160: A previous catch clause already catches all exceptions of this or a super type ('Exception')

Catching Exception itself is a last resort, for the very top of a program or a log of errors you didn't expect. A catch (Exception) around everything hides bugs you'd want to know about.

finally

A finally block runs after the try, whether an exception was thrown or not, and even when it was caught:

try
{
    Console.WriteLine("Saving...");
    int.Parse("abc");
}
catch (FormatException)
{
    Console.WriteLine("Couldn't save");
}
finally
{
    Console.WriteLine("Done");
}
Saving...
Couldn't save
Done

It's for cleaning up, such as closing a file. You'll rarely need it, because using closes files and connections for you even when an exception is thrown, as you'll see in Reading a File in C#.

Prevent what you can check

Exceptions are for the exceptional. If you can check for a problem with an if, check:

Instead of catching Check first
IndexOutOfRangeException if (index < scores.Length)
DivideByZeroException if (count > 0)
FormatException from int.Parse int.TryParse, which returns false instead of throwing
string[] inputs = { "12", "abc", "7" };

foreach (string input in inputs)
{
    if (int.TryParse(input, out int quantity))
    {
        Console.WriteLine(quot;Ordered {quantity}");
    }
    else
    {
        Console.WriteLine(quot;'{input}' isn't a number");
    }
}
Ordered 12
'abc' isn't a number
Ordered 7

Checking is clearer, and much faster than throwing and catching. Keep try and catch for what you can't check beforehand. Files are the classic case: even after File.Exists says yes, another program could delete or lock the file a moment later. So code that reads a file always goes in a try.

Throwing your own

Your own methods can throw exceptions too. When a method is asked to do something it can't, such as an order of zero items, throw an exception rather than carrying on with bad data:

try
{
    AddToOrder("Coffee", 0);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}

void AddToOrder(string item, int quantity)
{
    if (quantity < 1)
    {
        throw new ArgumentException("Quantity must be at least 1.", nameof(quantity));
    }
    Console.WriteLine(quot;Added {quantity} x {item}");
}
Quantity must be at least 1. (Parameter 'quantity')

throw stops the method at once, and the exception travels back to whichever code called it, until some catch handles it. Pick the type that says what went wrong:

Exception Throw it when
ArgumentException A parameter's value isn't allowed
ArgumentNullException A parameter is null and mustn't be
InvalidOperationException The object can't do that right now, such as starting a ride that's already started

Check yourself

3 questions to try on the lesson page.

So what

Check for what you can, catch what you can't, and throw when your own code is asked to do the impossible. An exception should be a clear message about something truly unexpected, never a crash your users have to report.

Key terms

Cheat sheets