You might have read, that re-throwing an exception like this: throw exc;
is considered bad practice and you should just do this: throw;
instead.
But why is it like that? For that, we have to discuss some core principles of exception handling. Let's see a small example here:
try {
Throws();
}
catch (Exception exc)
{
Console.WriteLine("ohoh an exception");
throw;
}
void Throws() => throw new Exception("??");
This will output something along the like this:
ohoh an exception
Exception
System.Exception: ??
at Program.<<Main>$>g__Throws|0_0()
at Program.<Main>$(String[] args)
Now let's do the same, but we are "re-throwing" the exception object itself:
try {
Throws();
}
catch (Exception exc)
{
Console.WriteLine("ohoh an exception");
throw exc;
}
void Throws() => throw new Exception("??");
The output:
ohoh an exception
Exception
System.Exception: ??
at Program.<Main>$(String[] args)
Wait a second? There is something missing! Our Throw
method is not in the stack-trace anymore! And that is exactly the difference when you re-throw like that. You will cut off the stack-trace until the catch block where you re-throw the object. Now there are valid cases where you really want this, mainly because of security concerns. I will not discuss this here in more detail.
Why does that happen?. To answer this you have to understand one important thing: Exceptions are not immutable! Let's have a look at the IL-code for this simplified version (and ignore the fact that the code itself is pretty dumb):
try {
throw new Exception("??");
}
catch (Exception exc)
{
throw exc;
}
catch
{
throw;
}
will be translated to:
.try
{
IL_0000: nop
IL_0001: ldstr "\ud83d\udca3" // This is ?? in Unicode
IL_0006: newobj instance void [System.Runtime]System.Exception::.ctor(string)
IL_000b: throw // This is the throw new Exception("") part
} // end .try
catch [System.Runtime]System.Exception
{
IL_000c: stloc.0
IL_000d: nop
IL_000e: ldloc.0
IL_000f: throw // This is the throw exc part
} // end handler
catch [System.Runtime]System.Object
{
IL_0010: pop
IL_0011: nop
IL_0012: rethrow // This is the throw; part
} // end handler
Two things are important here: The throw new Exception
and throw exc;
do have the same IL code. Only throw;
has a different IL-Code: rethrow
. Here is where this mutability part comes into play. You as a developer, you don't have to set the stack-trace at all. So someone else has to do it and you can guess who is doing that: the throw
keyword does that for you. So yes that is all the magic.
Rethrowing with the original stack trace
I told you earlier that there are valid use cases to handle an exception and rethrow it later. If you still want to have the whole stack trace there is a helper class for you: ExceptionDispatchInfo
. It offers a Throw
method, which re-throws the saved exception with the given stack-trace.
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:
Exception
System.Exception: ??
at Program.<<Main>$>g__Throws|0_0()
at Program.<Main>$(String[] args)
--- End of stack trace from previous location ---
at Program.<Main>$(String[] args)