Exception Handling Using Try and Catch

Exception Handling Using Try and Catch

Exception Handling

Exception handling is a crucial step in making your software more resilient to unexpected behaviour. Exception handling defines what should be done when an error is encountered, and if a different action is required depending on what went wrong.

Try Blocks

Code can be written within a try code block. This means that if something in that code block fails, execution will look for a corresponding catch or finally block which defines the way that the error is handled, thus making the error graceful.

If all the statements in the try-catch run successfully and without error then neither the catch nor finally blocks are hit.

This is the execution order for the blocks, either one of the catch and finally blocks must exist if a try block is present:

  1. Try Block
  2. Catch Block (If Exists)
  3. Finally Block (If Exists)

If an error is encountered and execution enters the catch block, the block has access to an exception object, often called e or ex. This exception object will contain information about the error that occurred.

The finally block is the last to be executed and usually contains code that cleans up the objects in the try statement. An example of this is closing an I/O connection that might have been opened.

Try and Catch example
Execution route of try, catch and finally blocks

Catch blocks can be further divided by the type of exception.


                    using System;

public class Program
{
	public static void Main()
	{
		try
		{
				Console.WriteLine("In try Block");
				int[] intArray = {1,2,3,4,5};
				Console.WriteLine(intArray[7]);
		}
		catch(NullReferenceException e)
		{
			Console.WriteLine("Will not be hit");
			Console.WriteLine(e.GetType());
		}
		catch(Exception e)
		{
			Console.WriteLine("In catch block");
			Console.WriteLine(e.GetType());
		}

		finally
		{
			Console.WriteLine("In finally block");
		}
	}
}

                

By defining behaviour for a different type of exception, we have more control over mitigating the impact of the issue. For example, if there is a divide by zero error, we might want to warn the user that they can not use this value.

Try, catch and finally blocks propagate downwards, if an error occurs in a function that is called from within a try block then the error will be passed upwards and checked in the caller.

You can catch any exception as long as it is, or a subtype of System.Exception. In circumstances where you might not know what is likely to go wrong, you can catch System.Exception itself which will catch all errors. Alternatively, you can try and catch System.Exception as a last resort after specifying a catch block for other types of exception. This is shown in the above example.

If you wish to catch everything you can omit both the exception type and the variable like so:

try
{
    //try code
} catch {
    //catch every error
}

If you do not need to access the properties of the error, you do not have to specify a variable, just the type of exception.

Exception Filters and Exception Handling

Exception filters provide more flexibility to exception handling, allowing a way to specify extra conditions that must be met before entering that catch block. By adding the ‘when’ keyword after the type of exception, you can specify extra conditionals. An example of this would be checking the status of the raised exception.

The Finally Block

Whether or not an exception is thrown inside the try block, the code within the final block will always run. Often, clean up code is included within this block and may have such jobs as closing a connection to I/O or a database.

The Using Statement

The using statement provides a method of ensuring that disposable classes are used correctly, this approach means you do not manually have to call dispose on an object that implements the IDisposable interface.

An example of this is the StringReader class:


                    using System;
using System.IO;

public class Program
{
	public static void Main()
	{
		string loremIpsumTest=@"Lorem ipsum dolor sit amet, consectetur adipiscing elit.
		Mauris ultrices aliquet purus, nec vulputate mauris tincidunt ac.
		Vestibulum at gravida leo. Fusce risus lacus, mattis ac ex vel, ultricies tincidunt est.";

		using (var reader = new StringReader(loremIpsumTest))
		{
			string? inputString;
			do {
				inputString = reader.ReadLine();
				Console.WriteLine(inputString);
			} while(inputString != null);
		}
	}
}
                

When execution is within the code block of the using statement, the target object is read-only and cannot undergo modification or reassignment.

The using statement will ensure that the dispose method is called under all circumstances (avoiding infinite loops). Dispose is even called when an exception occurs – the same result as putting the dispose action in a finally block.

If you wish to declare multiple objects in the using statement, you will have to use a comma to separate each object. For example:


                    using System;
using System.IO;

public class Program
{
	public static void Main()
	{
		string numberList=@"1
		2
		3
		4.";
		string letterList=@"a
		b
		c
		d";

		using (StringReader numbers = new StringReader(numberList),
			letters = new StringReader(letterList))
		{
			string? inputString;
			do {
				inputString = numbers.ReadLine();
				Console.Write(inputString);
				Console.Write("    ");
				inputString = letters.ReadLine();
				Console.WriteLine(inputString);
			} while(inputString != null);
		}
	}
}
                

From C# 8 onwards, you can use a new form of syntax for the using statement which does not require you to define a code block immediately after the statement.

More information on how to do this can be found on the Microsoft Documentation for C#:

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement

Manually Throwing Exceptions

As well as being thrown by the runtime, it is also possible for exceptions to be thrown from user-written code. To do this, make use of the ‘throw’ keyword.


                    using System;

public class Program
{
	public static void Main()
	{
		RepeatWord("Hi");
		RepeatWord("");
	}

	public static void RepeatWord(string targetWord)
	{
		if (string.IsNullOrWhiteSpace(targetWord))
		{
			throw new ArgumentNullException($"The variable targetWord was empty or whitespace.");
		} else {
		    Console.WriteLine($"{targetWord} {targetWord}");
		}
	}

}


                

You might also want to throw an exception if you have not yet implemented a method. By using an expression-bodied function you can do this in one line:

public void FancyNewFunction() => throw new NotImplementedFunction("TODO");

Rethrowing Exceptions

In some circumstances, you may want to do something with the captured exception and still output it at a later point in its unaltered form. To do this we rethrow an exception. Rethrowing means that we capture the exception in a catch block, do something with the exception object and then throw the original – this will consume the exception.

You might want to do this if you, or your organisation, is using a custom logging system. Rethrowing will allow you to pass the exception object to the logger and then rethrow it to the console.

Alternatively, you may not think that the thrown error was appropriate for the scenario. Rethrowing allows you to throw the exception as a different type.

Common Exception Types

There are a few types of Exception that you are likely to encounter frequently. These exceptions can either be thrown by the runtime or by user code. The exceptions you may encounter are:

ArgumentException

This exception is thrown when one of the arguments that handed to a method is not valid.

ArgumentNullException

This exception is thrown when an object that has a null reference is passed to a method.

ArgumentOutOfRangeException

This exception is thrown when the value of an argument is outside of the allowable range of the method.

InvalidOperationException

This exception is thrown when an object is used in conjunction with an unsupported method.

NotSupportedException

This exception is thrown when an invoked method is not supported in the current context.

NotImplementedException

This exception is thrown when the desired method has not yet been implemented.

ObjectDisposedException

This exception is thrown when a method is called on an object that has already been disposed of.

Key Information from Exceptions

When we capture exceptions, there is some key information we look to gather from them. By understanding this information, we gain additional insight into the nature of the issue in our software.

Where the Exception Occurred

A property in all exception objects is the StackTrace property. This is a string that represents all of the methods that are called down to the origin of the exception.

Summary of the Issue

the Message property provides a short message containing the description of the error that occurred.

Inner Exceptions

The InnerException property contains information about whether the exception was called inside an inner method and what effect it had on OuterExceptions