Sometimes our application crashes due to unexpected errors and exceptions that occur during the program execution. So, in this article, we are going to discuss the try-catch block in C# and learn how to handle those exceptions.
Let’s start.
What is Try-Catch in C#
By using a try-catch block in C#, we are handling exceptions that could happen in our code. Exceptions are problems in our application that are predictable while errors are not. Possible exceptions include opening an unreachable file, manipulating a null object, and dividing a number by zero. When these exceptions occur, the system will raise them.
Sometimes, our end-user may trigger exceptions in our application based on their actions. Other times, it could be due to the environmental context our code is running on. Whatever the cause, such exceptions may crash our application if not properly handled using try-catch.
The try
block should contain any suspicious code that may likely produce an exception. We should take note that the C# compiler will flag any try
block with no catch
or finally
block. Similarly, the catch
block contains any action or operation we want to execute when the code in the try
block raises an exception. Logging is an example of such action. For instance, when receiving arguments with a method we may want to log and throw an exception if the expected values are null. We should use a specific exception (ArgumentException) rather than the global Exception object for our catch
handler.
Try-Catch Flow
A try-catch consists of a try
block followed by one or more catch
clauses, which specify handlers for different exceptions. The CLR (Common Language Runtime) is responsible for handling exceptions in a running application.
The CLR takes several steps when an exception occurs. First, it checks to see if the method that raised the exception has a suitable catch
handler. If so, the flow of execution jumps to the handler, it executes the code in the block, and then the program continues its execution.
But if there is no catch
block, the CLR checks if the method that called the current method on the call stack has a suitable handler. The CLR does this for all methods on the call stack till it gets a suitable handler. When the CLR reaches the end of the call stack without finding a suitable handler, it will display an unhandled exception message to the user and terminate the application.
When should We Use Try and Catch?
Basically, we should use try-catch to prevent our application from crashing due to exceptions. In addition, we can use try-catch to display a custom message to the end-user based on some invalid input, unexpected inputs, or user actions. Going one step further, some third-party applications don’t handle exceptions so it is the job of the developer to handle any exceptions raised from its usage.
The Basic Try and Catch Block
Let’s take a look at how we can use a simple try-catch block in C#:
try { // the code here that may raise exceptions } catch { // handle an exception here }
A try
block can have multiple catch
blocks to handle the different types of exceptions that might occur during the program execution.
Specific exceptions should come first:
try { var counter = new int[5]; counter [10] = 12; } catch (IndexOutOfRangeException ex) { Console.WriteLine("{0} First exception caught.", ex.Message); } catch (Exception ex) { Console.WriteLine("{0} Last exception caught.", ex.Message); }
A catch
block can also throw a new exception or re-throw the current exception. The difference between a new exception and a re-thrown one lies in the preservation of the stack trace. Preservation of the stack trace provides means to trace the call to the line number in the method where the exception occurs. On one hand, a re-thrown exception preserves the original stack trace of the exception while a new exception doesn’t.
An exception is thrown using the throw
keyword, which may or may not be followed by a new instance of a class that derives from System.Exception
:
try { int[] counter = null; throw new NullReferenceException("Undefined array"); } catch (NullReferenceException ex) { throw; } catch (Exception ex) { throw new ApplicationException("Exception thrown"); }
The first catch
block handles any null exception raised in the try block. When the try
block raises a null exception, the CLR goes to the first catch
block and re-throw the exception with the call stack preserved. While for the second catch
block, we create another exception using the throw
keyword. For this we use a new instance of ApplicationException
that derives from System.Exception
.
Nested Try-Catch Block
A nested try-catch block is a try-catch in another try-catch block. It is important to note that while using nested try-catch blocks, the first suitable catch
block that follows the try
block will handle any raised exception:
try { var result = 100 / divider; try { var array = new int[0]; result = array[result]; } catch (IndexOutOfRangeException ex) { Console.WriteLine("{0} First exception caught.", ex.Message); throw; } } catch (DivideByZeroException e) { Console.WriteLine("Outer catch", e.Message); throw; }
This code sample will raise a DivideByZeroException
exception if divider
is set as zero. This occurs because the inner catch
cannot handle the raised exception. On the other hand if the divider
is greater than zero IndexOutOfRangeException
will be raised.
Invalid Catch Block
When we have a try block followed by the more than one general catch block, we call that catch block an Invalid Catch Block:
try { int[] a = new int[5]; a[10] = 12; } catch { Console.WriteLine("{0} Exception caught."); } catch (Exception ex) { Console.WriteLine("{0} Exception caught.",ex.Message); }
The last catch block will be flagged as the CS1017 compiler error. This is because both are general catch
of a try
block
Furthermore, to make it valid, a general catch
block must be the last one:
try { var result = 3000 / divider; Math.Sign(double.NaN); } catch (DivideByZeroException ex) { Console.WriteLine("Inner catch", ex.Message); } catch { Console.WriteLine("Outer catch"); }
Another way to add a general catch
is by creating a catch
block with an Exception
parameter:
try { var result = 3000 / divider; Math.Sign(double.NaN); } catch (DivideByZeroException ex) { Console.WriteLine("Inner catch", ex.Message); throw; } catch (Exception ex) { Console.WriteLine("Outer catch", ex.Message); throw; }
Exception Filters
The exceptions filter feature makes it possible to add a condition to a catch
block. By applying a filter to a catch
block, the code in the block is implemented if the condition is true. To create a filtered catch
block, we have to append the when
keyword with the required condition:
try { return words[index]; } catch (IndexOutOfRangeException e) when (index < 0) { throw new IndexOutOfRangeException( "index of an array cannot be negative."); } catch (IndexOutOfRangeException e) { throw new IndexOutOfRangeException( "index cannot be greater than the array size."); }
When the value of index
is less than zero, IndexOutOfRangeException
is raised and the control of execution flows to the first catch
block. Similarly, if the value of the index
is greater than the length of the words
array IndexOutOfRangeException
is also raised but the control of execution flows to the second catch
block as a result of the filter added to the first catch block.
Conclusion
In this article, we’ve learned about the try-catch block in C#, and when to use it. We’ve also answered some questions on its implementation in our code.