Skip to Content
Course content

39: Exceptions and Try/Catch/Finally

Click on the "Edit" button in the top corner of the screen to edit your slide content.

What's actually happening when I "throw" an exception?

Think of a try block as a designated "danger zone." You're telling the .NET runtime, "I'm about to do something that might blow up, so please keep a close eye on it." When something goes wrong—like trying to divide by zero or accessing a file that doesn't exist—the runtime stops the normal flow of your code immediately and creates an Exception object. This object contains the "what," "where," and "why" of the crash.

If you have a catch block, the runtime jumps straight there. It's essentially a detour. If you don't have one, the exception bubbles up to the method that called your current method, and if it keeps bubbling up without being caught, your app simply crashes. I've spent way too many hours debugging "unhandled exceptions" because I assumed a library method would handle its own errors—don't make that mistake. Always assume external calls can fail.

try 
{
    // Imagine this method reads a config file from the disk
    string connectionString = File.ReadAllText("appsettings.txt");
    Console.WriteLine("File read successfully!");
}
catch (FileNotFoundException ex)
{
    Console.WriteLine($"I couldn't find the file: {ex.Message}");
}

Can I handle different types of errors in different ways?

Absolutely, and you should. Catching the generic Exception class is a bit like using a sledgehammer to hang a picture frame—it works, but it's overkill and messy. You want to be as specific as possible. You can stack multiple catch blocks under a single try, and C# will check them from top to bottom.

The key here is the order. You must put the most specific exceptions first and the most general ones last. If you put catch (Exception ex) at the top, it will catch everything, and your more specific blocks below it will never even be looked at. The compiler will actually give you a warning if you get this order wrong.

try 
{
    string input = File.ReadAllText("settings.txt");
    int timeout = int.Parse(input); // This could throw a FormatException
}
catch (FileNotFoundException)
{
    Console.WriteLine("The settings file is missing. Using defaults.");
}
catch (FormatException)
{
    Console.WriteLine("The file exists, but the timeout value isn't a valid number.");
}
catch (Exception ex)
{
    Console.WriteLine($"Something weird happened that I didn't expect: {ex.Message}");
}

Why do I need a "finally" block if I'm already catching the error?

The finally block is your guarantee. Whether the code in the try block finished perfectly or the catch block just spent five minutes handling a disaster, the finally block will execute. This is where you put cleanup code.

I usually use this for things like closing database connections, shutting down network streams, or releasing file locks. If you open a file in the try block and an exception occurs before you hit the Close() method, that file might stay locked by your operating system until the entire program shuts down. That's a nightmare for the user. Putting the cleanup in finally ensures the door is closed no matter how you exited the room.

StreamReader reader = null;
try 
{
    reader = new StreamReader("data.txt");
    Console.WriteLine(reader.ReadToEnd());
}
catch (IOException ex)
{
    Console.WriteLine($"Disk error: {ex.Message}");
}
finally 
{
    // This runs even if an exception was thrown AND caught
    if (reader != null) 
    {
        reader.Close();
        Console.WriteLine("Stream closed safely.");
    }
}

Should I just wrap my entire Main method in one giant try/catch?

Please, don't do that. It's tempting because it prevents the app from crashing, but it's a terrible way to write software. When you wrap everything in one giant block, you lose the "granularity" of the error. You won't know if the crash happened because of a database timeout, a null reference in a loop, or a bad user input.

Instead, wrap only the specific lines of code that are likely to fail. Keep your try blocks small. This makes it much easier to debug and allows you to recover gracefully. For example, if a single item in a list of 1,000 fails to process, you can catch the exception inside the loop, log the error for that one item, and move on to the next one. If the try was outside the loop, one bad piece of data would kill the entire process for all 1,000 items.




📋 Practical Task

Exercise: Robust Configuration Value Parser

You are building a system that reads a "Port Number" from a simulated external source (a string). You need to create a method called GetPortNumber() that handles the following scenarios without crashing the program:

  • If the string is null or empty, throw a InvalidOperationException with the message "Configuration source is empty".
  • If the string is not a valid integer, catch the FormatException and return a default port of 8080.
  • If any other unexpected error occurs, catch it and print the error message to the console.
  • Regardless of what happens, print "Port lookup attempt completed" at the very end of the process.

Requirements: Use a try-catch-finally structure. You can simulate the "external source" by passing a string into your method (try testing it with "80", "abc", and null).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.