Exceptions & Exception Handling in C#

Types of Errors – Review from Debugging

  • Syntax errors: compile time issues, indicated by red squiggles, prevent program execution.
    • Example: Typographical errors like forgetting to capitalize “L” in WriteLine.
  • Execution time errors: cause the program to crash during runtime.
    • Example: Attempting to convert a non-integer string input to an integer.
  • Program logic errors: the program runs without crashing but produces incorrect results.
    • Example: Calculating an average by dividing the sum by an incorrect number (e.g., dividing by 5 instead of 3).
    • Expecting an output of 4.0 ( (3+4+5=12/3=4)(3 + 4 + 5 = 12 / 3 = 4)) but getting 2.4.

Exceptions

  • Exception: An object of a class generated due to an execution-time error or unexpected event.
  • Exceptions are "thrown" during such events.
  • Unhandled exceptions typically crash the program.

Handling Exceptions

  • Exception handler: C# code that responds to exceptions gracefully (i.e., without crashing).
    • Graceful handling includes:
      • Correcting the problem and continuing execution.
      • Displaying a message to alert the user for corrective action.
      • Saving information, displaying a clear error message, and terminating the program.
  • Exception handling: The process of intercepting and responding to exceptions.
  • C# provides a default exception handler that displays an error message and crashes the program when an exception is not handled.
  • It is recommended to handle any exceptions that may occur.

Types of Exceptions

  • C# defines numerous exception types, forming a hierarchy of Exception classes.
    • Exception: The most general type, encompassing all other exception types.
    • DivideByZeroException: Occurs when dividing by zero.
    • FormatException: Occurs when something is poorly formatted.
      • Example: Passing the string “abc” to int.Parse.
    • IndexOutOfRangeException: Occurs when trying to access an array element outside its bounds.
    • NullReferenceException: Occurs when attempting to call a method on a null object.
  • Custom exception types can also be defined.

Handling Exceptions with try-catch Block

  • The try-catch block is used to handle exceptions in C#.
    • Syntax:
try {
    // Code that may crash goes here…
} catch(<Exception type> <parameter name>) {
    // Code for what to do instead of crashing goes here…
}

try Block

  • The try block contains code that will be attempted and monitored for exceptions.
  • Correct code can encounter exceptional conditions due to:
    • Bad user input.
    • Out of memory errors.
    • Disk issues (full, read-only, offline).
    • Hardware failures.
    • Network problems.
    • Security issues blocking access.
  • The try block contains one or more code statements that may throw an exception.
  • If an exception is thrown within the try block, the program will intercept and handle it in the catch block.
  • If code outside of a try block throws an exception, it cannot be handled by a catch block, leading to program termination.

catch Block

  • Each try block is followed by one or more catch blocks.
  • A catch block starts with:
    • catch (<Exception type> <parameter name>)
      • Exception type: The name of the exception class (e.g., Exception, FormatException).
      • Parameter name: A name to refer to the exception object within the catch block (commonly "e" or "ex").
      • The exception type and parameter name are optional; you can use just the catch keyword.
  • The code within a catch block is executed only if the try block throws an exception.

Exception Handling Flow

  • If an exception is thrown within a try block:
    • The corresponding catch block is executed.
    • After the catch block, the program continues executing the next line following the catch block.
  • If no exception is thrown within a try block:
    • The catch block is skipped.
    • The program continues executing the next line following the catch block.

Example: Without Exception Handling

  • Without exception handling, the default exception handler is called, which crashes the program.
  • An IndexOutOfRangeException occurs when trying to access an array element with an invalid index.

Example: With Exception Handling

  • An array numbers is created with three elements: 3,9,2{3, 9, 2}.
  • A try block attempts to iterate through the array and print each element.
  • A catch block handles the IndexOutOfRangeException that occurs when the loop tries to access an invalid index.
  • The program prints a message indicating the subscript is beyond the array's size and continues execution.

Retrying When Exception is Thrown

  • A while loop is used to continuously prompt the user for an integer input until a valid integer is entered.
  • The try block attempts to parse the user's input using int.Parse().
  • If the parsing fails (i.e., a FormatException is thrown), the catch block is executed, informing the user to try again.
  • If the parsing is successful, the ok flag is set to true, and the loop exits.

Exceptions – Message Field

  • Exceptions are objects with fields and methods.
  • Each exception object has a Message field.
    • Message: Retrieves a message indicating what error occurred.

Polymorphic References to Exceptions

  • Exception classes form a hierarchy (inheritance or "is-a" relationship).
  • A polymorphic reference can be used as a parameter in the catch block.
  • Catching a type of exception also catches any exception type below it in the hierarchy.
    • Example: ArgumentNullException and ArgumentOutOfRangeException are derived from ArgumentException, which is derived from SystemException, which is derived from Exception.
    • Catching ArgumentException will catch ArgumentException, ArgumentNullException, and ArgumentOutOfRangeException.
    • Catching Exception will catch any exception type.

Polymorphic References to Exceptions - Examples

  • Example 1: Catching Exception
int number;
try
{
    number = int.Parse(Console.ReadLine()); // throws a FormatException, if non-integer input given
}
catch (Exception ex) // FormatException is derived from Exception, this catch block will catch it
{
    Console.WriteLine($"The following error occurred: {ex.Message}");
}
  • Example 2: Catching FormatException
int number;
try
{
    number = int.Parse(Console.ReadLine()); // throws a FormatException, if non-integer input given
}
catch (FormatException ex) // FormatException is the exception that might be thrown, it will be caught
{
    Console.WriteLine($"The following error occurred: {ex.Message}");
}
  • Example 3: Catching IndexOutOfRangeException (Will not be caught)
int number;
try
{
    number = int.Parse(Console.ReadLine()); // throws a FormatException, if non-integer input given
}
catch (IndexOutOfRangeException ex) // IndexOutOfRange Exception is unrelated to FormatException, it will not be caught
{
    Console.WriteLine($"The following error occurred: {ex.Message}");
}

Handling Multiple Exceptions

  • The code within the try block may throw more than one type of exception.
  • Different exceptions may need to be handled differently using multiple catch blocks.
  • catch blocks must be listed from most specific (lower on the hierarchy) to most general (higher on the hierarchy) exception type.
  • There cannot be multiple catch blocks for the same exception type.
  • Only one catch block is executed.

Handling Multiple Exceptions - Invalid Examples

  • You cannot have two catch blocks for the same exception type because it is ambiguous which one should be executed.
  • You cannot list a more general exception type before a more specific one because the more general handler would catch everything that the second handler could catch.

Handling Multiple Exceptions - Valid Example

int number;
string str = "";
try
{
    str = Console.ReadLine();
    number = int.Parse(str); // throws a FormatException, if non-integer input given
}
catch (FormatException ex) // more specific
{
    Console.WriteLine($"{str} is not a number.");
}
catch (SystemException ex) // more general
{
    Console.WriteLine("Bad number format.");
}
  • If the exception types are at the same level in the hierarchy, their order does not matter.

Handling Exceptions – finally block

  • The finally block is optional and can be included after the last catch block in a try-catch structure.
  • The code within the finally block is always executed, regardless of whether an exception was thrown or not.
try {
    // Code that may crash goes here…
} catch(<Exception type> <parameter name>) {
    // Code for what to do instead of crashing goes here…
} finally {
    // Code to execute, regardless of whether an exception occurs
}
  • A common use case is ensuring a file gets closed.
  • Even when an unhandled exception occurs, causing the program to crash, the finally block will still execute before the program terminates.

The Call Stack and Stack Trace

  • When a program executes, the system tracks the execution history.
    • Example: If method m1 invokes method m2, the system remembers where it was in m1 to return when m2 finishes.
  • If a program crashes, the execution history tells you where the crash occurred and how the program got there.
  • Call stack: An internal list of all methods currently executing.
  • Stack trace: A list of all methods in the call stack (the execution history).
  • Reviewing the stack trace allows you to determine:
    • The method executing when the exception occurred.
    • The methods called to reach that point.

The Call Stack and Stack Trace - Details

  • The stack trace is displayed when the default exception handler is called.
  • The stack trace is also accessible through the exception object’s StackTrace field.

Uncaught Exceptions

  • When an exception is thrown, it cannot be ignored.
  • It must be handled by the program or the default exception handler.
  • When code in a method throws an exception:
    • Normal execution of that method stops.
    • C# searches for a compatible exception handler inside the method.
  • If no matching handler exists in the method:
    • Control is passed back to the previous method in the call stack (the invoking method).
    • If that method has no handler, control is passed up the call stack again.
  • If control reaches the Main method:
    • The Main method must handle the exception, or
    • The program halts, and the default exception handler takes over.

Throwing Exceptions

  • You can throw your own exceptions when an exceptional condition is detected using the throw keyword.
  • The exception can be one of the built-in C# exception types (e.g., Exception, FormatException) or a custom type.
  • The parameter passed to throw is the value for the Message field of the exception object.

Defining Your Own Exception Types

  • Defining custom exception types requires inheritance from the Exception class.
  • You need to define a new class that inherits from Exception and provide definitions for the constructors.
  • You can then throw exceptions of this type when needed.
  • Example: Throwing an AgeException in a Person's age setter if the user enters an invalid age.
  • Exceptions provide a better way of handling invalid input in classes' setters instead of defaulting to some arbitrary value.