Exceptions explained
Back to articles.The previous article explained the purpose of exceptions and their benefits. This article aims at describing exception classes and how you use them.
A gentle introduction
We will go through the exception class in detail soon, but let's start with a small usage example. Everything will be explained in detail later in the article.
try
{
throw new InvalidOperationException("Failed to load resource 'Hello world!'.");
}
catch (InvalidOperationException exception)
{
Console.WriteLine(exception.Message);
}
A new exception object of the type InvalidOperationException
is
created in the above example. It's constructed with an error message
stating that the resource could not be loaded. The purpose of the error
message is to explain why we decided to throw an exception.
Next, we use the throw
keyword with the created exception object.
throw
tells .NET to stop running the code and send the exception up
the call stack until it finds a catch
block in any of the invoked
methods. That is also called exception propagation.
Finally, we have a try/catch
block. The purpose of the catch
block
is to tell .NET that we want to handle a specific type of exception. The
exception can have been thrown inside the try
block or any called
method within the try
block.
The output from the application will be the following.
Conclusion
- The message in the exception constructor should be used to explain why an exception was thrown.
throw
is used to abort the current execution flow when a method cannot deliver what it promised.try
is used to tell which code any potential exceptions should be handled for.catch
is used to handle exceptions, including all exceptions that inherit the specified one.
Exception classes
Exception classes are used to represent exceptions. The base class,
Exception
, defines some properties that all exceptions will share.
Exception class names should always end with Exception
per .NETs
naming conventions, like InvalidDataException
or
SecurityException
.
There are two properties on the Exception
class that are more
important than the other ones. Those two are specified below, to learn
about the rest of the properties, read the
documentation
in MSDN.
Message
The first property is the Message
property since it typically explains
why the exception was thrown.
The property value is in most cases specified in the constructor of an exception:
new KeyNotFoundException("Failed to find the given key `meta` in the dictionary.");
new EntityNotFoundException("User 'Lars' was not found.");
new ArgumentException("UserId must be alphanumeric.");
StackTrace
The stack trace is like breadcrumbs. It tells where in the code that the exception originated from. The first line in the stack trace is the method that the exception was thrown in. The next line is the method that called the failing method and so on.
Let's take the earlier code example, but with complete source and with the stack trace printed instead.
using System;
namespace Exception1
{
class Program
{
static void Main(string[] args)
{
try
{
throw new InvalidOperationException("Failed to load resource 'Hello world!'.");
}
catch (Exception exception)
{
Console.WriteLine(exception.StackTrace);
}
}
}
}
The code will generate the following output:
at Exception1.Program.Main(String[] args) in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 11
The output shows that the exception was thrown in the Main
method in
the class Program.cs
. The line number is included since this
application had the
pdb
file distributed with it.
Conclusion
- Exceptions are classes that inherit the
Exception
base class. - The
StackTrace
property shows where in the application that the exception was thrown and how that method was called. - The
Message
property specifies why an exception was thrown. - To get line numbers in the stack trace, include all
pdb
files from the build output.
Exception propagation
The stack trace is essential when wanting to understand where an exception was thrown. It shows which path the code took in the application before the exception was thrown. Having that knowledge makes it easier to reproduce the cause of the generated the exception.
The stack trace is never prepopulated but is instead appended each time the .NET exception handling mechanism travels up the call stack. That process is called exception propagation.
Consider the following example:
var myException = new InvalidOperationException("Failed to load resource 'Hello world!'.");
Console.WriteLine(myException.StackTrace);
The output from exception.StackTrace
will be null
because it's the
throw
statement that starts to fill the stack trace.
Wrapping the example in a try/catch will populate the stack trace:
using System;
namespace Exception1
{
class Program
{
static void Main(string[] args)
{
try
{
var myException = new InvalidOperationException("Failed to load resource 'Hello world!'.");
throw myException;
}
catch (Exception exception)
{
Console.WriteLine(exception.StackTrace);
}
}
}
}
Output
at Exception1.Program.Main(String[] args) in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 12
Likewise, calling a method from the try block will produce two lines in the stack trace.
using System;
namespace Exception1
{
class Program
{
static void Main(string[] args)
{
try
{
AnotherMethod();
}
catch (Exception exception)
{
Console.WriteLine(exception.StackTrace);
}
}
public static void AnotherMethod()
{
throw new InvalidOperationException("Failed to load resource 'Hello world!'.");
}
}
}
Output
at Exception1.Program.AnotherMethod() in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 21
at Exception1.Program.Main(String[] args) in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 11
Observe that the Main()
method is last in the stack trace. It's
because it was appended as the stack unwinds from the deepest call to
the first one.
Having three calls works the same.
using System;
namespace Exception1
{
class Program
{
static void Main(string[] args)
{
try
{
AnotherMethod();
}
catch (Exception exception)
{
Console.WriteLine(exception.StackTrace);
}
}
public static void AnotherMethod()
{
DeepestMethod();
}
public static void DeepestMethod()
{
throw new InvalidOperationException("Failed to load resource 'Hello world!'.");
}
}
}
Output
at Exception1.Program.DeepestMethod() in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 26
at Exception1.Program.AnotherMethod() in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 21
at Exception1.Program.Main(String[] args) in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 11
DeepestMethod
did not have a try block, nor did AnotherMethod
but
Main
did. Thus the exception traveled three steps before getting to a
catch
block.
Having a catch block inside AnotherMethod
would stop the exception
propagation:
using System;
namespace Exception1
{
class Program
{
static void Main(string[] args)
{
try
{
AnotherMethod();
}
catch (Exception exception)
{
Console.WriteLine(exception.StackTrace);
}
}
public static void AnotherMethod()
{
try
{
DeepestMethod();
}
catch (Exception exception)
{
Console.WriteLine("Handled in AnotherMethod: " + exception.Message);
}
}
public static void DeepestMethod()
{
throw new InvalidOperationException("Failed to load resource 'Hello world!'.");
}
}
}
Output
Notice that the console writes from Main
is not printed, since
exception propagation stops at the first found catch
block.
Conclusions
- Exception propagation is when the exception travels up the method calls (i.e. stack frames).
- The stack trace is appended for each method call that the exception travels to.
- Exception propagation stops when a
catch
block is found.
Rethrowing exceptions
Sometimes you want to catch exceptions to be able to process them in some way but still let the upper stack frames be able to handle the same exception.
That can be achieved by having another throw
in the catch block.
using System;
namespace Exception1
{
class Program
{
static void Main(string[] args)
{
try
{
AnotherMethod();
}
catch (Exception exception)
{
Console.WriteLine(exception.StackTrace);
}
}
public static void AnotherMethod()
{
try
{
throw new InvalidOperationException("Sample exception");
}
catch (Exception ex)
{
Console.WriteLine("Exception was here: " + ex);
Console.WriteLine();
throw;
}
}
}
}
Output
Exception was here: Sample exception
at Exception1.Program.AnotherMethod() in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 28
at Exception1.Program.Main(String[] args) in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 11
The first two lines are from the Console.WriteLine
calls in
AnotherMethod
and the stack trace is from Console.WriteLine
in
Main()
.
Using throw
in catch blocks tells .NET that it should continue with
the exception propagation.
Avoid throw ex;
when rethrowing exceptions
Do not use throw exception;
in catch blocks:
try
{
throw new InvalidOperationException("Sample exception");
}
catch (Exception ex)
{
throw ex;
}
Using throw ex;
tells .NET that it should start a new exception
propagation using your previously created exception object. As in such,
the stack trace will be cleared and built from scratch again.
A complete example:
using System;
namespace Exception1
{
class Program
{
static void Main(string[] args)
{
try
{
AnotherMethod();
}
catch (Exception exception)
{
Console.WriteLine(exception.StackTrace);
}
}
public static void AnotherMethod()
{
try
{
DeepestMethod();
}
catch (Exception ex)
{
Console.WriteLine("Exception was here: " + ex.Message);
Console.WriteLine();
throw ex;
}
}
public static void DeepestMethod()
{
throw new InvalidOperationException("Failed to load resource 'Hello world!'.");
}
}
}
Output
Exception was here: Failed to load resource 'Hello world!'.
at Exception1.Program.AnotherMethod() in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 28
at Exception1.Program.Main(String[] args) in C:\src\junk\Exceptions\Exception1\Exception1\Program.cs:line 11
See? The DeepestMethod
is nowhere to be found in the stack trace,
making it impossible to find where the exception originated from.
Conclusion
- Rethrowing can be done to process the exception before letting it continue to travel up the call stack.
- Avoid
throw ex
as it tells .NET to treat the exception object as a new exception.