Exceptions¶
This chapter looks closer to exceptions and how exceptions are handled.
Re-throwing exceptions incorrectly¶
If done the wrong way, the stack trace including valuable information is gone.
❌ Bad Throwing the original object again will create a new stack trace
try
{
// logic which can throw here
}
catch (Exception exc)
{
throw exc;
}
⚠️ Warning Creating a new exception object with the original as InnerException
but a new stack trace which can make debugging harder.
try
{
// logic which can throw here
}
catch (Exception exc)
{
throw new Exception("A message here", exc);
}
✅ Good Simply throw
the exception to preserve the original stack trace.
try
{
// logic which can throw here
}
catch (Exception exc)
{
// Logic like logging here
throw;
}
💡 Info: Sometimes hiding the original stack trace might be the goal due to for example security related reasons. In the average case re-throwing via
throw;
is preferred.
Pokemon Exception Handling¶
Indiscriminately catching exceptions. “Pokemon - gotta catch ‘em all”
❌ Bad Catching every exception and gracefully swallowing it.
try
{
MyOperation();
}
catch (Exception exc)
{
// Do nothing
}
Also:
try
{
MyOperation();
}
catch
{
// Do nothing
}
✅ Good Catch the specific exception one would expect and handle it.
try
{
MyOperation();
}
catch (InvalidOperationException exc)
{
logger.Log(exc);
// More logic here and if wished re-throw the exception
throw;
}
Throwing NullReferenceException
, IndexOutOfRangeException
, and AccessViolationException
¶
This exceptions should not be thrown in public API’s. The reasoning is that those exceptions should only be thrown by the runtime and normally indicate a bug in the software.
For example one can avoid NullReferenceException
by checking the object if it is null
or not. On almost any case there different exceptions one can utilize, which have more semantic.
❌ Bad Throw NullReferenceException
when checking a parameter.
public void DoSomething(string name)
{
if (name == null)
{
throw new NullReferenceException();
}
✅ Good Indicating the argument is null and use the proper exception.
public void DoSomething(string name)
{
ArgumentNullException.ThrowIfNull(name);
}
💡 Info: More details can be found here.
Don’t throw StackOverflowException
or OutOfMemoryException
¶
Both exceptions are meant only to be thrown from the runtime itself. Under normal circumstances recovering from a StackOverflow
is hard to impossible. Therefore catching a StackOverflowException
should also be avoided.
And can catch OutOfMemoryException
as they can also occur if a big array gets allocated but no more free space is available. That does not necessarily mean recovering from this is impossible.
💡 Info: More details can be found here.
Exception filtering with when
¶
The when
expression was introduced with C# 6 and allows a more readable way of filtering exceptions.
❌ Bad Less readable way.
try
{
await GetApiRequestAsync();
}
catch (HttpRequestException e)
{
if (e.StatusCode == HttpStatusCode.BadRequest)
{
HandleBadRequest(e);
}
else if (e.StatusCode == HttpStatusCode.NotFound)
{
HandleNotFound(e);
}
}
✅ Good More readable way.
try
{
await GetApiRequestAsync();
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.BadRequest)
{
HandleBadRequest(e);
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
{
HandleNotFound(e);
}
Finalizers should not throw exceptions¶
Throwing an exception in a finalizer can lead to an immediate shutdown of the application without any cleanup.
❌ Bad Throwing an exception in the finalizer.
class MyClass
{
~MyClass()
{
throw new NotImplementedException();
}
}
✅ Good Don’t throw exceptions.
class MyClass
{
~MyClass()
{
}
}
💡 Info: More details can be found here.
Preserving the stack trace of an exception¶
When catching an exception and re-throwing the exception via throw exc
later the stack-trace gets altered, but sometimes capturing the exception can make sense. Since the .NET Framework 4.5 there is a small helper named ExceptionDispatchInfo
, which let’s you preserve that information. It offers a Throw
method, which re-throws the saved exception with the given stack-trace.
❌ Bad Re-throwing the exception while losing the stack-trace
Exception? exception = null;
try
{
Throws();
}
catch (Exception exc)
{
exception = exc;
}
// Do something here ...
if (exception is not null)
throw exception;
void Throws() => throw new Exception("💣");
Produces the following output:
Unhandled exception. System.Exception: 💣
at Program.<Main>$(String[] args) in /Users/stgi/repos/Exception/Program.cs:line 15
✅ Good Capturing the context to re-throw it later.
using System.Runtime.ExceptionServices;
ExceptionDispatchInfo? exceptionDispatchInfo;
try
{
Throws();
}
catch (Exception exc)
{
exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exc);
}
// Do something here ...
if (exceptionDispatchInfo is not null)
exceptionDispatchInfo.Throw();
void Throws() => throw new Exception("💣");
Produces the following output:
Unhandled exception. System.Exception: 💣
at Program.<<Main>$>g__Throws|0_0() in /Users/stgi/repos/Exception/Program.cs:line 19
at Program.<Main>$(String[] args) in /Users/stgi/repos/Exception/Program.cs:line 7
--- End of stack trace from previous location ---
at Program.<Main>$(String[] args) in /Users/stgi/repos/Exception/Program.cs:line 17