Throwing exceptions - Why is my stack trace lost?

10/21/2022

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)

Missing Stack trace when eliding the await keyword

You may have heard that when you elide the await keyword in a method that returns a Task or Task<T>, you lose the stack trace. Buy why does that happen? Let's find out!

Leverage 'is not' Operator for Exception Filtering!

Did you know you can use the 'is not' operator with exception filtering to simplify and improve your error handling in C#?

In this short blog post, I will show you how to use it.

Help my memory dump always shows me some exceptions!

I made a memory dump in my simplest console application and there are a bunch of exception instances around, what is going on? Let’s see in this blog post, why you see a few exception instances in your memory dump.

An error has occurred. This application may no longer respond until reloaded. Reload x