Why throw is Preferred Over throw ex in C

In C#, handling and rethrowing exceptions correctly is critical for effective debugging and application stability. While both throw; and throw ex; propagate an exception up the call stack, throw; is the standard best practice because it preserves the original stack trace. In contrast, throw ex; resets the stack trace to the location where the rethrow occurs, effectively erasing the history of where the error truly originated.

Understanding the Difference

When an exception occurs, the runtime builds a stack trace that records the sequence of method calls leading directly to the failing line of code. How you rethrow that exception determines whether this vital forensic information is kept or destroyed.

Using throw;

Using throw; (without an identifier) tells the runtime to continue bubbling up the existing exception object.

try
{
    DoRiskyOperation();
}
catch (Exception ex)
{
    // Log the error or perform cleanup
    Log(ex);
    
    // Preserves the original stack trace
    throw; 
}
  • Preserves Call History: The original stack trace remains untouched. If DoRiskyOperation() calls three nested private methods before failing, the stack trace will point directly to the line inside the deepest method where the failure occurred.
  • Simplifies Debugging: Developers reading the error logs can pinpoint the exact line of code responsible without guessing.

Using throw ex;

Using throw ex; explicitly throws the caught variable as if it were a newly instantiated exception at that line.

try
{
    DoRiskyOperation();
}
catch (Exception ex)
{
    Log(ex);
    
    // Resets the stack trace to this line
    throw ex; 
}
  • Overwrites the Stack Trace: The runtime discards the origin history and resets the stack trace root to the throw ex; statement itself.
  • Hides the Real Culprit: Any context about which method or line inside DoRiskyOperation() threw the exception is permanently lost. This turns production bug diagnostics into a time-consuming guessing game.

Alternative: Wrapping in a New Exception

If you need to add higher-level context to the error rather than simply rethrowing it, instantiate a new exception and pass the caught exception as the innerException:

try
{
    DoRiskyOperation();
}
catch (Exception ex)
{
    throw new OrderProcessingException("Failed to process order.", ex);
}

This pattern preserves the original stack trace via the InnerException property while providing caller-friendly context.

Summary

Never use throw ex;. When handling an exception that you cannot fully resolve in the current scope, use throw; to retain the complete diagnostic history, or wrap it in a new exception using the inner exception pattern.