4

I'm flabbergasted. I always thought that throw by itself in a catch block would throw the exception at hand without altering the stack trace, but that throw ex in a catch block would alter the stack trace to show the exception originating at the location of the statement.

Take the following two blocks of code. I would expect the output to be subtly different because one uses throw and the other uses throw ex, but the output is identical between the two, and the actual source line that incited the initial exception is lost in both cases, which seems terrible to me. What am I missing?

This first example behaves as I would expect:

using System;
                    
public class Program
{
    public static void Main()
    {
        try
        {
            DummyWork();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
        
    private static void DummyWork()
    {
        try
        {
            throw new Exception("dummy");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            throw ex;  // I would expect to lose the information about the inciting line 5 above this one in this case.... and I do.
        }
    }
}

This second example behaves identical to the first, but I DO NOT expect that:

using System;
                    
public class Program
{
    public static void Main()
    {
        try
        {
            DummyWork();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
        
    private static void DummyWork()
    {
        try
        {
            throw new Exception("dummy");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            throw;  // I would NOT expect to lose the information about the inciting line 5 above this one in this case.... But I do.  Output is identical.
        }
    }
}

UPDATE: Some commenters have said they can't repro this - here is my dot fiddle (you will have to manually edit it to go back and forth between the two versions): https://dotnetfiddle.net/Mj7eK5

UPDATE #2: In answer to some commenters who have asked for the "identical" output. Here is the output from the first example:

System.Exception: dummy
   at Program.DummyWork() in d:\Windows\Temp\xoyupngb.0.cs:line 21
System.Exception: dummy
   at Program.DummyWork() in d:\Windows\Temp\xoyupngb.0.cs:line 26
   at Program.Main() in d:\Windows\Temp\xoyupngb.0.cs:line 9

And here is the output from the second example:

System.Exception: dummy
   at Program.DummyWork() in d:\Windows\Temp\jy4xgqrf.0.cs:line 21
System.Exception: dummy
   at Program.DummyWork() in d:\Windows\Temp\jy4xgqrf.0.cs:line 26
   at Program.Main() in d:\Windows\Temp\jy4xgqrf.0.cs:line 9

Leaving aside the insignificant temp file differences, in both cases the outer catch (the second one) is missing the line 21 of the initial throw. I would expect that in the first example throw ex but not in the second throw.

Stephan G
  • 3,289
  • 4
  • 30
  • 49
  • I can't reproduce this. The second example preserves the full stack trace as expected. – David L Sep 06 '22 at 22:56
  • I'm not able to replicate the described results. In the final stack trace for the first example, I'm pointed to lines 26 and 9. In the final stack trace for the second example, I'm pointed to lines 21 and 9. The `throw ex;` lost the information that the exception originated on line 21 (`throw new Exception("dummy");`, and changed it to have originated on line 26 (`throw ex;`). Which is exactly what I would expect and exactly what you were originally expecting. – David Sep 06 '22 at 22:56
  • If you want to see the difference add `static void SecondLevelDummy() => throw new Exception("dummy");` and call `SecondLevelDummy();` inside the `try` block in `DummyWork`. – 41686d6564 stands w. Palestine Sep 06 '22 at 22:57
  • 1
    Duplicate of [Is there a difference between "throw" and "throw ex"?](https://stackoverflow.com/questions/730250/is-there-a-difference-between-throw-and-throw-ex) To Quote: _"`throw ex` resets the stack trace (so your errors would appear to originate from `HandleException`"_. In your case, `HandleException` is `DummyWork`. You don't see a difference because the exception originates from `DummyWork` anyway. – 41686d6564 stands w. Palestine Sep 06 '22 at 22:58
  • 1
    I'm baffled at folks who can't reproduce this. Here is my dot fiddle - currently set to the second example, but you can easily edit to push it to the first example, and back and forth, and the output is the same..... https://dotnetfiddle.net/Mj7eK5 – Stephan G Sep 06 '22 at 23:01
  • @DavidL The second block says _"Output is identical"_. I believe that's the main problem. The part about _"lose the information about the inciting line 5"_ is unclear and is probably a misunderstanding on the OP's part. Since the OP didn't post the exact stack trace, we can't really say for sure. – 41686d6564 stands w. Palestine Sep 06 '22 at 23:02
  • @StephanG: Honestly this leads me to suspect that .NET Fiddle may be doing something unknown to us under the hood, as unlikely as I would have considered that. It could also be specific to the compiler being used? What happens when you test it locally, removing that 3rd party from the equation? – David Sep 06 '22 at 23:06
  • @David - the whole reason I did a dotFiddle in the first place is that I ran into this in my own code working in a task in an ASP.NET application and was just dumfounded. But I suppose I can try in my own VS, please stand by. – Stephan G Sep 06 '22 at 23:12
  • @David - just did as you suggested in my own console app on my own computer, same code, same behavior. Same surprise. – Stephan G Sep 06 '22 at 23:17
  • @41686d6564standsw.Palestine - Your "SecondLevelDummy" did seem to do the trick. So I'm grateful for that. But I don't understand and would appreciate further insights on WHY that is needed. In the real world, we can't always know what line of code will cause an exception and make sure we put that in a separate method..... – Stephan G Sep 06 '22 at 23:25
  • I think there was some misunderstanding as to what you were seeing. Based on your update, it looks like you actually have the full stack trace in both examples. It doesn't remove any of the stack trace. – David L Sep 06 '22 at 23:31
  • The reason for this is because you are in the same method as the throwing line, so there's no stack trace to "reset". You've never left the stack. – David L Sep 06 '22 at 23:34
  • @StephanG I've posted an answer below. – 41686d6564 stands w. Palestine Sep 06 '22 at 23:47
  • Would this be considered a duplicate of https://stackoverflow.com/q/39778515/120955? – StriplingWarrior Sep 06 '22 at 23:48
  • 1
    Looks like the behaviour is different for .NET Framework (4.7.2) and .NET 6. The latter works as expected but the former indeed swallows stack in `throw` case, not sure why. – Ed'ka Sep 07 '22 at 00:17
  • 1
    Yeap, looks like it was [fixed in .NET Core 2.1](https://github.com/dotnet/runtime/issues/9518) but not back-ported to .NET Framework. – Ed'ka Sep 07 '22 at 01:07

1 Answers1

7

Note: This answer is for .NET Framework. You might observe a different behavior if you're using .NET Core or .NET 5.0 and above, as mentioned in the comments. I did not test on all versions of .NET Core


Okay, let me take a stab at it. The difference between throw and throw ex is already explained in Is there a difference between "throw" and "throw ex"? but I'll try to put it in more clear terms to fit the narrative of this question.

  • throw ex: Rethrows the exception from this point and resets the stack trace.

  • throw: Rethrows the exception from this point and preserves the stack trace.

Let's look at the code in question:

private static void DummyWork()
{
    try
    {
        throw new Exception("dummy");  // Line 21
    }
    catch (Exception ex)
    {
        throw;  // Line 25
    }
}

Here, whether we use throw or throw ex, the stack trace will always be:

at Program.DummyWork() in ...:line 25
at Program.Main() in ...:line 9

Q: Why "line 25"?

A: Because both throw and throw ex rethrow the exception from that point.

Q: Why is there no difference in this case?

A: Because there aren't any more stack frames in the stack trace to reset.

Q: How can we see the difference?

Well, let's add another level to generate another stack frame. The code would be something like this:

private static void DummyWork()
{
    try
    {
        MoreDummyWork();  // Line 21
    }
    catch (Exception ex)
    {
        throw;  //Line 25
    }
}

private static void MoreDummyWork() 
{
    throw new Exception("dummy");  // Line 31
}

Here, we can clearly see the difference. If we use throw, the stack trace is the following:

at Program.MoreDummyWork() in ...:line 31
at Program.DummyWork() in ...:line 25
at Program.Main() in ...:line 9

But if we use throw ex, the stack trace becomes:

at Program.DummyWork() in ...:line 25
at Program.Main() in ...:line 9

Q: Okay, you say both will throw the exception from that point. What if I want to maintain the original line number?

A: In this case, you can use ExceptionDispatchInfo.Capture(ex).Throw(); as explained in How to rethrow InnerException without losing stack trace in C#?:

  • While this is all true, I don't know if it's a complete answer: the same code in a different version of .NET (say .NET 6) will leave the original stack trace untouched, showing that the exception was thrown on line 21, as the OP expected. – StriplingWarrior Sep 06 '22 at 23:38
  • @StriplingWarrior that isn't universally true, since that behavior can be suppressed in .NET 6 with `[System.Diagnostics.StackTraceHidden]`, which is perhaps what you are referring to. In .NET 6, the default behavior is the same, to my knowledge. – David L Sep 06 '22 at 23:40
  • This is awesome info, and I'll mark it the answer. `ExceptionDispatchInfo.Capture(ex).Throw()` works well for me, as there isn't a general solution to ensuring that an unexpected exception will happen to be thrown in a different method. – Stephan G Sep 06 '22 at 23:46
  • @DavidL: in my testing, StackTraceHidden removes the entire DummyWork method from the StackTrace. But without that attribute I'm seeing the stack trace showing the line number of the original `throw new ...` statement. I think this is why so many commenters had a hard time reproducing the issue: the behavior differs from .NET Framework to .NET Core. – StriplingWarrior Sep 06 '22 at 23:46
  • @StriplingWarrior ahh I see your point. That’s interesting and it definitely explains the difficulty in reproducing. – David L Sep 07 '22 at 00:20
  • It also depends on whether it is compiled in Debug or Release: in Debug there is `line 31` but in Release there is no difference between `throw` and `throw ex`. I suspect that in Release compiler rewrites this code heavily. Probably worth checking the actual IL generated to understand why. – Ed'ka Sep 07 '22 at 00:39