Iron Academy Logo
Learn C#

C# Exception Handling

Tim Corey
59m 46s

Exception handling is a crucial aspect of robust application development. Tim Corey’s video on "Handling Exceptions in C# - When to catch them, where to catch them, and how to catch them," provides a detailed explanation of what exceptions are, how to handle them, and where to handle them.

This article aims to explain Exception handling in C# using Tim Corey's video. It is a powerful feature that allows developers to manage errors and exceptional conditions that arise during program execution. Using the try, catch, and finally blocks, C# provides a structured way to handle runtime errors, log exceptions, and maintain program flow.

Introduction

Tim begins by explaining that many developers have an incorrect view of exceptions and their handling. He emphasizes the importance of understanding what exceptions are and where and how to handle them properly to create more robust applications.

Building a Demo Console Application

Tim creates a console application in Visual Studio 2017 to demonstrate exception handling. He recommends using console applications for testing new topics as they require minimal setup and are easy to work with.

using System;

namespace ExceptionsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.ReadLine();
        }
    }
}
using System;

namespace ExceptionsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.ReadLine();
        }
    }
}

Creating a Class Library

Tim adds a class library to the solution to simulate a real-world scenario where different methods call each other.

Csharp Exception Handling 1 related to Creating a Class Library

He deletes the default class and creates a new class named DemoCode.

public class DemoCode
{
    public int GetNumber(int position)
    {
        int[] numbers = { 1, 4, 7, 2 };
        return numbers[position];
    }

    public int ParentMethod(int position)
    {
        return GetNumber(position);
    }

    public int GrandparentMethod(int position)
    {
        return ParentMethod(position);
    }
}
public class DemoCode
{
    public int GetNumber(int position)
    {
        int[] numbers = { 1, 4, 7, 2 };
        return numbers[position];
    }

    public int ParentMethod(int position)
    {
        return GetNumber(position);
    }

    public int GrandparentMethod(int position)
    {
        return ParentMethod(position);
    }
}

The DemoCode class contains methods that call each other, ultimately retrieving a number from an array based on the given position.

Simulating an Exception

Tim explains that the application is intended to demonstrate failures rather than successes. He introduces an out-of-bounds exception by passing an invalid position to the GrandparentMethod.

DemoCode demo = new DemoCode();
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");
DemoCode demo = new DemoCode();
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");

Running the above code with an invalid position results in an IndexOutOfRangeException. Tim shows how the Visual Studio debugger highlights the issue and provides detailed information about the exception.

How NOT to Use try-catch

Tim explains a common mistake developers make when they first learn about try-catch blocks. They often wrap the entire block of code where they expect an exception might occur, which can lead to improper handling.

try
{
    int output = 0;
    output = numbers[position];
    return output;
}
catch (Exception ex)
{
    return 0;
}
try
{
    int output = 0;
    output = numbers[position];
    return output;
}
catch (Exception ex)
{
    return 0;
}

Tim highlights that this approach is problematic because it hides the exception and continues execution with incorrect assumptions. For example, returning 0 as a default value might not be appropriate and can cause further issues.

Correct Exception Handling

Tim stresses that exceptions provide critical information about unexpected states in the application. If the application continues in such a state without proper handling, it can lead to further errors and data corruption.

Instead of swallowing exceptions, it is essential to handle them appropriately. Here is a better approach:

try
{
    return numbers[position];
}
catch (Exception ex)
{
    // Log the exception or handle it appropriately
    Console.WriteLine(ex.Message);
    throw; // Re-throw the exception to be handled by a higher-level handler
}
try
{
    return numbers[position];
}
catch (Exception ex)
{
    // Log the exception or handle it appropriately
    Console.WriteLine(ex.Message);
    throw; // Re-throw the exception to be handled by a higher-level handler
}

By re-throwing the exception, you ensure that the issue is propagated and can be handled at a higher level if necessary.

Providing Useful Information to the User

Tim explains that while some exceptions can be handled gracefully without crashing the application, it is important to provide useful feedback to the user. For example, showing a message box or a notification with an option to retry the operation.

More Useful Information: StackTrace

Tim demonstrates how to use the StackTrace property of the exception object to get detailed information about where the exception occurred. This includes the class, method, and line number, which is invaluable for debugging.

try
{
    return numbers[position];
}
catch (Exception ex)
{
    Console.WriteLine(ex.StackTrace);
    throw;
}
try
{
    return numbers[position];
}
catch (Exception ex)
{
    Console.WriteLine(ex.StackTrace);
    throw;
}

The StackTrace property provides a complete trace of the call stack, helping developers pinpoint the exact location of the issue.

Proper Placement of try-catch

Tim explains that handling exceptions properly is not just about catching them but also about knowing where to place your try-catch blocks. The key is to place try-catch blocks at a level where you have enough context to handle the exception appropriately.

Example of Improper Placement

Placing a try-catch block deep in the call stack often doesn't allow you to handle the exception effectively because you lack the context of the higher-level operations.

// Deep level exception handling (not ideal)
try
{
    return numbers[position];
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    throw;
}
// Deep level exception handling (not ideal)
try
{
    return numbers[position];
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    throw;
}

Example of Proper Placement

Placing the try-catch block at the top level, such as in the user interface or entry point of the application, allows you to handle exceptions with the full context of the operation.

try
{
    int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
    Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}
try
{
    int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
    Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}

This way, you can provide more informative messages to the user and decide whether the application can continue running or if it should be terminated.

Stack Trace Information

Tim emphasizes the importance of stack trace information in diagnosing exceptions. The stack trace provides a detailed call history, showing where the exception occurred and the chain of method calls that led to it.

try
{
    int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
    Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}
try
{
    int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
    Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}

This output gives the exact location of the exception and the path taken through the code, making it easier to debug and fix the issue.

Handling Logic Demonstration

Tim demonstrates how to handle logic at the appropriate level. For example, if a method is responsible for opening and closing a database connection, it should handle exceptions to ensure resources are properly managed.

public int GrandparentMethod(int position)
{
    try
    {
        Console.WriteLine("Open database connection");
        int output = ParentMethod(position);
        Console.WriteLine("Close database connection");
        return output;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw; // Ensure the exception is propagated
    }
}
public int GrandparentMethod(int position)
{
    try
    {
        Console.WriteLine("Open database connection");
        int output = ParentMethod(position);
        Console.WriteLine("Close database connection");
        return output;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw; // Ensure the exception is propagated
    }
}

In this example, if an exception occurs, the database connection is not properly closed, leading to potential resource leaks. By adding a try-catch block, you can ensure that the connection is closed even if an exception occurs.

Using the finally Block

Tim introduces the finally block, which ensures that certain code runs regardless of whether an exception occurs. This is particularly useful for cleaning up resources, such as closing database connections.

public int GrandparentMethod(int position)
{
    try
    {
        Console.WriteLine("Open database connection");
        int output = ParentMethod(position);
        return output;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw; // Re-throw the exception to ensure it's handled by a higher-level handler
    }
    finally
    {
        Console.WriteLine("Close database connection");
    }
}
public int GrandparentMethod(int position)
{
    try
    {
        Console.WriteLine("Open database connection");
        int output = ParentMethod(position);
        return output;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw; // Re-throw the exception to ensure it's handled by a higher-level handler
    }
    finally
    {
        Console.WriteLine("Close database connection");
    }
}

The finally block runs after the try and catch blocks, ensuring that the connection is closed even if an exception is thrown.

The throw Statement

Tim explains the importance of re-throwing exceptions to pass them up the call stack. This allows higher-level handlers to process the exceptions appropriately.

catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    throw; // Re-throws the exception to be handled by the calling method
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    throw; // Re-throws the exception to be handled by the calling method
}

Re-throwing the exception with throw; ensures that the full stack trace is preserved, providing valuable context for debugging.

Properly Bumping Up Exceptions

Tim demonstrates how exceptions bubble up through the call stack. Each method checks for a try-catch block and either handles the exception or passes it up to the caller.

try
{
    int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
    Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}
try
{
    int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
    Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}

In this example, the GrandparentMethod catches the exception, logs it, and re-throws it. The top-level try-catch block in the console application then handles the exception and displays the error message and stack trace.

Common Mistakes in Exception Handling

Tim highlights several common mistakes developers make when handling exceptions:

  1. Using throw ex;:

    • Rewriting the stack trace and losing valuable context.

    • Example:
      catch (Exception ex)
      {
          // Incorrect
          throw ex; // Rewrites stack trace
      }
      catch (Exception ex)
      {
          // Incorrect
          throw ex; // Rewrites stack trace
      }
  2. Throwing a New Exception:

    • Creating a new exception with a custom message but losing the original stack trace.

    • Example:
      catch (Exception ex)
      {
          // Incorrect
          throw new Exception("I blew up");
      }
      catch (Exception ex)
      {
          // Incorrect
          throw new Exception("I blew up");
      }

Creating a New Exception without Losing the Original Stack Trace

Tim explains how to create a new exception while preserving the original stack trace. This can be useful when you want to provide a more meaningful error message or a different exception type while still retaining the context of the original error.

catch (Exception ex)
{
    throw new ArgumentException("You passed in bad data", ex);
}
catch (Exception ex)
{
    throw new ArgumentException("You passed in bad data", ex);
}

By passing the original exception (ex) as the inner exception, you maintain the original stack trace, which is crucial for debugging.

Preserving Stack Trace Information

Tim demonstrates how to access the original exception's message and stack trace when creating a new exception.

catch (Exception ex)
{
    Console.WriteLine("You passed in bad data");
    Console.WriteLine(ex.StackTrace);
    throw new ArgumentException("You passed in bad data", ex);
}
catch (Exception ex)
{
    Console.WriteLine("You passed in bad data");
    Console.WriteLine(ex.StackTrace);
    throw new ArgumentException("You passed in bad data", ex);
}

This ensures that the exception thrown up the stack contains both the new message and the original exception details.

Looping Through Inner Exceptions

Tim provides a method to loop through all inner exceptions to extract their messages and stack traces.

catch (Exception ex)
{
    Exception inner = ex;
    while (inner != null)
    {
        Console.WriteLine(inner.StackTrace);
        inner = inner.InnerException;
    }
    throw;
}
catch (Exception ex)
{
    Exception inner = ex;
    while (inner != null)
    {
        Console.WriteLine(inner.StackTrace);
        inner = inner.InnerException;
    }
    throw;
}

This loop iterates through each inner exception, printing its stack trace, ensuring that all layers of exceptions are accounted for.

Handling Different Exceptions Differently

Tim discusses how to handle different types of exceptions using multiple catch blocks. This allows for specific handling based on the exception type.

try
{
    // Code that might throw an exception
}
catch (ArgumentException ex)
{
    Console.WriteLine("You gave us bad information. Bad user!");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}
try
{
    // Code that might throw an exception
}
catch (ArgumentException ex)
{
    Console.WriteLine("You gave us bad information. Bad user!");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}

In this example, ArgumentException is handled specifically by printing a custom message, while all other exceptions fall back to a general handler that prints the exception message and stack trace.

Csharp Exception Handling 2 related to Handling Different Exceptions Differently

Importance of Order in Multiple Catch Blocks

Tim emphasizes the importance of order when using multiple catch blocks. The most specific exceptions should be caught first, followed by more general exceptions.

try
{
    // Code that might throw an exception
}
catch (ArgumentException ex)
{
    Console.WriteLine("You gave us bad information. Bad user!");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}
try
{
    // Code that might throw an exception
}
catch (ArgumentException ex)
{
    Console.WriteLine("You gave us bad information. Bad user!");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
}

If a more general catch block appears before a specific one, it will catch all exceptions, and the specific catch block will never be reached, leading to compilation errors.

Conclusion

Tim Corey’s advanced video guide on exception handling in C# covers critical techniques for creating new exceptions, preserving stack traces, and using multiple catch blocks effectively. By following his best practices, developers can create robust applications that handle exceptions gracefully and provide valuable debugging information.