Throwing exceptions - Why is my stack trace lost?

21/10/2022
C#.NETException

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)
6
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x