🔧 Your Rethrown Exception Lost the Stack Trace That Actually Pointed at the Bug
An exception is caught, logged or wrapped, and rethrown – and the stack trace that shows up afterward points at the RETHROW line, not at where the exception actually originated, because `throw ex;` resets the exception’s stack trace to start fresh from that point, discarding exactly the information you’d need to find the real problem.
🔎 The Problem
try
{
ParseOrderFile(path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse order file");
throw ex; // <- resets the stack trace to THIS line
}
// The stack trace in the rethrown exception now starts at "throw ex;"
// inside this catch block - it no longer shows which line inside
// ParseOrderFile actually threw, which is usually the one piece of
// information you actually need to diagnose the failure.
✅ Fix: Use throw; (No Expression) to Preserve the Original Trace
- Change `throw ex;` to just `throw;` – the bare `throw` statement rethrows the SAME exception object with its ORIGINAL stack trace fully intact, rather than treating it as a new throw from the current location.
- If wrapping the exception with additional context is the goal (a common and reasonable pattern), pass the original as the `innerException` parameter: `throw new OrderProcessingException(“Failed to parse order file”, ex);` – this preserves the original exception (and its trace) accessible via `.InnerException`, while still adding your own higher-level context.
⚠️ Why `throw ex;` Ever Seems Reasonable
- It reads naturally in code review – “catch this, then throw it again” – and it compiles and runs without any warning, which is exactly why this specific mistake shows up in codebases at every experience level; the difference between `throw;` and `throw ex;` is easy to overlook since both look like “just rethrow it”.
- `Exception.StackTrace` is actually a read-only property populated by the runtime during unwinding – the only way to reset it is by throwing the exception object again as if it were new, which is precisely the behavior `throw ex;` (as opposed to bare `throw;`) triggers.
throw ex isn’t rethrowing the exception – it’s throwing a new one that happens to look the same, and it leaves the trail behind exactly where the mistake was made.
