Updated on
Use throw;. It rethrows the exception we caught and keeps the stack trace pointing at the line that actually failed. throw ex; rethrows the same object but restarts the trace at the throw ex line, losing everything below it.
That lost part is the part we need. It is the frames between our catch block and the real failure: the third-party method, the null reference, the line number. Once throw ex; runs, no log and no debugger can get them back.
What Do throw and throw ex Actually Do?
Both statements rethrow the same exception object. The difference is what happens to its stack trace on the way out.
throw;, the bare form valid only inside a catch block, passes the exception along untouched. The trace still names the method that originally threw, and every frame between there and here.
throw ex; treats the caught exception as a brand-new throw. Microsoft’s analyzer rule CA2200 puts it exactly: “the stack trace is restarted at the current method”, so the caller sees the exception as though it originated at the throw ex; line, and every frame below that point is lost.
The consequence is a debugging one, not a correctness one. Both versions compile, both propagate the same exception type and message, and both look identical in a catch upstream. The difference only appears in the log, at three in the morning, when the trace stops at a method that did not fail.
Rethrowing the Original Exception – The Standard Way
We’re all familiar with this scenario – we have some code that might throw an exception. We want to act upon this exception (a.k.a. handle it), for example by cleaning up some resources or logging certain data. This does not mean we want to hide the exception – quite contrary, we want it to bubble up, that’s why we want to rethrow the exception:
public class BusinessWorker
{
public void Work_Throw()
{
try
{
//lots of other business logic all around...
new ThirdPartyComponent().DoInternalWork();
}
catch (Exception ex)
{
//here we would handle 'ex' in the BusinessWorker (clean up resources, log state, call 911 etc.)
throw;
}
}
}
This is the standard way of doing it, and we can be certain that any code that uses our BusinessWorker will receive all the available details about an exception. The tests here use NUnit, so Assert.AreEqual() is the assertion we reach for; if we copy them into an xUnit project, the assertion names change:
public void ThrowBehaviour_KeepsProperStackTrace()
{
try
{
new BusinessWorker().Work_Throw();
}
catch (Exception ex)
{
Assert.AreEqual(
@"System.InvalidOperationException: That's a nasty bug!
at ThrowVsThrowEx.ThirdPartyComponent.<GoDeeper>g__DoDangerousOperation|1_0()
at ThrowVsThrowEx.ThirdPartyComponent.GoDeeper()
at ThrowVsThrowEx.ThirdPartyComponent.DoInternalWork()
at ThrowVsThrowEx.BusinessWorker.Work_Throw()
at ThrowVsThrowEx.ThrowVsThrowExSamples.ThrowBehaviour_KeepsProperStackTrace()",
ex.ToString());
}
}
We see the exception and we immediately know that something has happened deep within a 3rd party component. For the wider picture around this, see our guide to exception handling in C#.
Rethrowing the Original Exception – The Incorrect* Way
The asterisk after “incorrect” is not accidental – there are valid situations where this is the desired behavior, and we come back to them at the end.
Now, what would happen if our code was only slightly different:
public class BusinessWorker
{
public void Work_ThrowEx()
{
try
{
//lots of other business logic all around...
new ThirdPartyComponent().DoInternalWork();
}
catch (Exception ex)
{
//here we would handle 'ex' in the BusinessWorker (clean up resources, log state, call 911 etc.)
throw ex;
}
}
}
Here we have used the throw ex statement instead of throw. The effect on the stack trace of our exception is dramatic:
public void ThrowEx_DropsTheStackTrace()
{
try
{
new BusinessWorker().Work_ThrowEx();
}
catch (Exception ex)
{
Assert.AreEqual(
@"System.InvalidOperationException: That's a nasty bug!
at ThrowVsThrowEx.BusinessWorker.Work_ThrowEx()
at ThrowVsThrowEx.ThrowVsThrowExSamples.ThrowEx_DropsTheStackTrace()",
ex.ToString());
}
}
Yes, dramatic is not an exaggeration, if we consider the time spent on looking for a nasty bug in the BusinessWorker.Work() method, instead of knowing right away where it happened. The same care applies when we are catching multiple exception types and rethrowing from one of several handlers.
Does throw ex Reset the Stack Trace?
Yes, and the traces printed above show exactly how much is lost.
The throw; version lists five frames, starting inside the third-party component where the failure actually happened. The throw ex; version lists two, starting at our own BusinessWorker method. Three frames, including the one naming the real bug, are gone.
This is why the compiler ships a rule for it. The .NET analyzers flag throw ex; inside a catch block precisely because it discards stack details, and the fix the rule suggests is to delete the two characters.
There is one objection worth answering. throw; is often said to overwrite the line number of the frame it rethrows from, since the rethrow point sits in that same method. On .NET 10 it does not: a method that catches and rethrows still reports the line that failed, not the line holding the throw;. Frames and line numbers both survive.
The rule is CA2200, “Rethrow to preserve stack details”, category Usage, and it is enabled by default in .NET 10 as a warning (Microsoft Learn, read 9 August 2026). Building this article’s sample on net10.0 confirms it: the compiler raises warning CA2200: Re-throwing caught exception changes stack information on the throw ex; line.
Wrapping The Original Exception And Throwing A New One
There is one more widely popular method for handling exceptions, especially in large systems. Exceptions can be wrapped into custom domain-specific exception types, in order to make debugging simpler in complex scenarios:
public class BusinessWorker
{
public void Work_WrapAndThrowNewEx()
{
try
{
//lots of other business logic all around...
new ThirdPartyComponent().DoInternalWork();
}
catch (Exception ex)
{
//here we would handle 'ex' in the BusinessWorker (clean up resources, log state, call 911 etc.)
throw new BusinessException("I am a business domain wrapper for internal exceptions.", ex);
}
}
}
In this case, we are wrapping the original exception into a new exception type – BusinessException. What happens with our stack trace when we throw a new exception?
public void WrapAndThrowNewEx_KeepsTheStackTraceInTheInnerException()
{
try
{
new BusinessWorker().Work_WrapAndThrowNewEx();
}
catch (Exception ex)
{
//the stack trace of the top level exception is short
Assert.AreEqual(@" at ThrowVsThrowEx.BusinessWorker.Work_WrapAndThrowNewEx()
at ThrowVsThrowEx.ThrowVsThrowExSamples.WrapAndThrowNewEx_KeepsTheStackTraceInTheInnerException()", ex.StackTrace);
//however, the actual exception and it's stack trace is visible within the ex.InnerException property
//the full exception string also reveals all the layers of inner exceptions
Assert.AreEqual(
@"ThrowVsThrowEx.BusinessWorker+BusinessException: I am a business domain wrapper for internal exceptions.
---> System.InvalidOperationException: That's a nasty bug!
at ThrowVsThrowEx.ThirdPartyComponent.<GoDeeper>g__DoDangerousOperation|1_0()
at ThrowVsThrowEx.ThirdPartyComponent.GoDeeper()
at ThrowVsThrowEx.ThirdPartyComponent.DoInternalWork()
at ThrowVsThrowEx.BusinessWorker.Work_WrapAndThrowNewEx()
--- End of inner exception stack trace ---
at ThrowVsThrowEx.BusinessWorker.Work_WrapAndThrowNewEx()
at ThrowVsThrowEx.ThrowVsThrowExSamples.WrapAndThrowNewEx_KeepsTheStackTraceInTheInnerException()",
ex.ToString());
}
}
That’s right – we are not losing any information, however, some of the data is now segregated into the InnerException of the BusinessException. Wrapping is the shape we want at an application boundary, which is also where handling exceptions globally with IExceptionHandler and serializing exceptions as JSON come into play.
How Do We Rethrow an Exception From Another Method?
throw; only works inside a catch block. When we have stored an exception and want to rethrow it later (from a different method, after awaiting something, out of a background worker), the bare form is not available.
throw ex; is the obvious substitute and it is the wrong one, for the reason above.
The right tool is ExceptionDispatchInfo. We capture the exception where we catch it, carry the capture wherever it needs to go, and call Throw() on it. The original stack trace survives, and the new frames are appended rather than replacing it, under a --- End of stack trace from previous location --- marker.
This is what the framework itself does. When an await rethrows the exception from a faulted task, it is not calling throw ex;: it is dispatching a captured exception, which is why an async stack trace still names the method that failed.
CA2200’s own page recommends the same tool this section does: if you rethrow from outside the handler, “use ExceptionDispatchInfo.Capture(Exception) to capture the exception in the handler”.
In code, that is five lines:
ExceptionDispatchInfo? captured = null;
try { new ThirdPartyComponent().DoInternalWork(); }
catch (Exception ex) { captured = ExceptionDispatchInfo.Capture(ex); }
captured?.Throw(); // original stack trace intact, here
The rethrow happens outside the catch block, which is the whole point, and the trace still opens on DoDangerousOperation() rather than on the line that called Throw().
When To Use Which Approach?
A rule of thumb for most cases would be to use the approach that does not hide the important debugging info – therefore, either go with “throw” or, if needed, wrap the exception. The exceptional case would be when we do not want to reveal the actual stack trace.
This is however an edge case and there are other measures that can be taken to prevent inner details from leaking. Often the better move is not to catch the exception at all, which is what filtering exceptions with the when keyword gives us.
throw; | throw ex; | throw new X("...", ex); | ExceptionDispatchInfo |
|
|---|---|---|---|---|
| Exception type seen by the caller | Original | Original | New wrapper type | Original |
| Original stack trace | Preserved | Restarted at the rethrow point | Preserved, in InnerException | Preserved |
| Original exception reachable | It is the exception | It is the exception | Via InnerException | It is the exception |
Valid outside a catch block | No | Yes | Yes | Yes |
| Analyzer warning | None | CA2200 | None | None |
| Use it when | Handling then letting it continue | Almost never | Adding domain context at a boundary | Rethrowing a captured exception later or elsewhere |
Conclusion
We have briefly touched upon three basic methods for rethrowing an exception in C# and what implications each approach has, plus the fourth one for rethrowing outside a catch block. For a more general overview, see our article on Exception Handling in C#.
Tested with .NET 10.0.10.

Hi. Please tag your code so that Google Translate does not translate it.
Fixed that for you. Thank you very much for the suggestion! Much appreciated.
or you can use the attribute translate=”no”
Yeah, there are several ways to do it. We’ve added a class that google translate looks for. Does it work for you now?
No. This page is translated entirely, including code blocks.
Can you try clearing the cache with Ctrl + F5?
Ok. Now the code is not translated. Thanks.
Excellent! Ty for the suggestion.
You’re welcome)
There is a slight issue with “throw”: It will alter the line-number of the current stack-row. Thus if you have two calls of “DoInternalWork()” within the try-catch, you will not know which of the two calls caused the error. The line-number will just point to the “throw;” and not to the call of “DoInternalWork()”. The Stack inside of “DoInternalWork()” will still be there. But you will not know the line-number, the call originated from.
Same issue if you have a NullReferenceException from within the try-catch (without calling any other methods). After “throw”, you will not be able to see the line number it originated from.
So you should just always wrap the exception into a new one.
Or, if you really want to have a “throw;”, use this extension by calling “e.Rethrow();”
public static void Rethrow(this Exception e) {
ExceptionDispatchInfo.Capture(e).Throw();
}
Good “catch” Andreas, no pun intended 🙂
Thank you guys please keep this up.
I am always looking forward to each and every article that you send.
Hey Zabron,
Thank you so much for the kind words. We’ll do our best!