throw in C#: Rethrow Without Losing the Truth

Preserving Stack Traces

If you've ever debugged an exception only to find the stack trace points to your catch block instead of where the error actually occurred, you've seen what happens when rethrowing loses diagnostic information. The throw keyword lets you rethrow exceptions without destroying their stack traces.

Use throw alone in a catch block to rethrow the current exception exactly as it was. This preserves the original stack trace, letting you see where the error started. Using throw ex creates a new throw point and loses that history. Understanding this distinction makes debugging production errors dramatically easier.

You'll build error handlers that log and rethrow exceptions correctly, preserving diagnostic data. By the end, you'll know when to rethrow, when to wrap, and how to maintain exception context through layers of code.

Rethrowing Correctly

Use throw without an argument to rethrow the caught exception with its original stack trace intact. This is the right way to log and propagate errors without hiding their source.

CorrectRethrow.cs
using System;

public class DataProcessor
{
    public void ProcessData(string data)
    {
        try
        {
            var result = ParseData(data);
            SaveResult(result);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error during processing: {ex.Message}");
            throw;
        }
    }

    private int ParseData(string data)
    {
        return int.Parse(data);
    }

    private void SaveResult(int result)
    {
        if (result < 0)
            throw new InvalidOperationException("Negative values not allowed");
    }
}

The catch block logs the error then rethrows with bare throw. The stack trace shows ParseData or SaveResult as the origin, not the catch block. This gives you accurate debugging information about where things actually broke.

The throw ex Anti-Pattern

Throwing the exception variable resets the stack trace to the current location. This hides where the error originated and makes debugging harder.

WrongRethrow.cs
using System;

public class BadExample
{
    public void ProcessData(string data)
    {
        try
        {
            DoSomethingRisky(data);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Logging: {ex.Message}");
            throw ex;
        }
    }

    private void DoSomethingRisky(string data)
    {
        throw new InvalidOperationException("Something went wrong");
    }
}

// Stack trace now points to the throw ex line,
// not to DoSomethingRisky where the problem occurred

Using throw ex loses the original stack trace. The exception now appears to originate from the catch block. This costs you valuable debugging information. Always use bare throw when rethrowing.

Wrapping Exceptions with Context

When crossing abstraction boundaries, wrap the original exception in a new one with added context. Set the InnerException to preserve the original cause and its stack trace.

WrappingExceptions.cs
using System;

public class FileProcessor
{
    public void ProcessFile(string filePath)
    {
        try
        {
            var content = ReadFile(filePath);
            ParseContent(content);
        }
        catch (Exception ex)
        {
            throw new ApplicationException(
                $"Failed to process file: {filePath}",
                ex);
        }
    }

    private string ReadFile(string path)
    {
        if (!File.Exists(path))
            throw new FileNotFoundException("File not found", path);
        return File.ReadAllText(path);
    }

    private void ParseContent(string content)
    {
        throw new FormatException("Invalid format");
    }
}

The new ApplicationException adds file path context while preserving the original FileNotFoundException or FormatException in InnerException. Callers see the high-level error with details about what failed, and debuggers show both stack traces.

Try It Yourself

Build a simple calculator that demonstrates proper rethrowing and exception wrapping. This shows how to preserve stack traces while adding context.

Steps

  1. Create: dotnet new console -n ThrowDemo
  2. Navigate: cd ThrowDemo
  3. Edit Program.cs
  4. Update .csproj
  5. Run: dotnet run
Program.cs
using System;

try
{
    var calc = new Calculator();
    calc.Divide(10, 0);
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
    Console.WriteLine($"Stack trace: {ex.StackTrace}");

    if (ex.InnerException != null)
    {
        Console.WriteLine($"Inner: {ex.InnerException.Message}");
    }
}

class Calculator
{
    public double Divide(double a, double b)
    {
        try
        {
            return PerformDivision(a, b);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(
                $"Division operation failed for {a}/{b}",
                ex);
        }
    }

    private double PerformDivision(double a, double b)
    {
        if (b == 0)
            throw new DivideByZeroException("Cannot divide by zero");
        return a / b;
    }
}
ThrowDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Run result

Error: Division operation failed for 10/0
Stack trace:    at Calculator.Divide...
Inner: Cannot divide by zero

Mistakes to Avoid

Swallowing exceptions silently: Catching and not rethrowing hides errors completely. If you can't handle an exception meaningfully, don't catch it. Let it propagate to code that knows how to deal with it. Empty catch blocks or catches that only log without rethrowing bury problems.

Using exceptions for control flow: Throwing exceptions for normal conditions like validation failures wastes performance. Use return values or result objects instead. Exceptions should signal unexpected errors, not expected branches in logic. Reserve throw for actual problems.

Throwing generic exceptions: Don't throw Exception or ApplicationException directly. Use specific exception types that describe the problem. Create custom exceptions when built-in types don't fit. Specific types let callers handle different errors appropriately and make debugging clearer.

Common Questions

What's the difference between throw and throw ex?

Use throw alone to rethrow the current exception with its original stack trace intact. Use throw ex to throw a new instance, which replaces the stack trace with the current location. Always prefer bare throw when rethrowing to preserve debugging information about where the error originated.

When should I wrap exceptions vs rethrowing?

Wrap exceptions when crossing abstraction boundaries to hide implementation details. Rethrow when you're logging or cleaning up but not changing context. Use InnerException when wrapping to preserve the original cause. Bottom line: rethrow for transparency, wrap for abstraction.

How do I add context when rethrowing?

Add to exception.Data before rethrowing or wrap with a new exception that has the original as InnerException. Use throw with custom messages to add context while preserving the stack. Exception filters with when clauses let you log before rethrowing without entering catch.

Is it safe to use throw in async methods?

Yes, throw works identically in async methods. The exception gets captured in the returned Task and rethrown when awaited. Use throw instead of returning Task.FromException for cleaner code. Task captures the exception and propagates it correctly through the async chain.

Back to Articles