AlgoMaster Logo

throw & Rethrow

Last Updated: May 17, 2026

14 min read

The throw statement is how your code raises an exception, and the way you raise or rethrow one decides what shows up in the stack trace your future self has to debug. This lesson covers raising exceptions from your own code, the two forms of rethrow (throw; and throw ex;), the antipattern that quietly destroys the original origin information, throw expressions in compact contexts, ExceptionDispatchInfo for preserving traces across asynchronous or cross-context boundaries, and the [DoesNotReturn] attribute that tells the compiler about helpers that never come back.

Throwing an Exception

Raising an exception from your own code is one statement: throw new SomeException(...). The runtime allocates the exception object, captures a stack trace starting at the throw site, and then begins unwinding the call stack looking for a matching catch. Until something catches it, the exception keeps propagating up.

The most common reason to throw is validation. Your method is asked to do something it cannot reasonably do, so it stops, signals the problem with the right exception type, and lets the caller decide how to react.

Two pieces of information land in the exception object. The message you wrote tells a human what went wrong, and the parameter name (passed via nameof(quantity)) tells tooling and callers which input caused the problem. The runtime appends the actual value because ArgumentOutOfRangeException accepts one in its constructor. None of this is magic, it is just constructor parameters traveling with the exception.

Picking the right exception type matters more than picking the right message. The Base Class Library already has the type for most validation problems you will ever throw, and using the right one means callers can write a precise catch clause instead of catching Exception and hoping. A few rules of thumb:

SituationThrow
Argument was nullArgumentNullException
Argument was outside an allowed rangeArgumentOutOfRangeException
Argument was the wrong shape for the operationArgumentException
Object is in a state that does not allow the callInvalidOperationException
A feature is intentionally not implemented yetNotImplementedException
The operation makes no sense for this typeNotSupportedException

Modern .NET has helpers that bundle the check and the throw into one call. ArgumentNullException.ThrowIfNull arrived in .NET 6, and ArgumentOutOfRangeException.ThrowIfNegative, ThrowIfZero, ThrowIfGreaterThan, and friends arrived in .NET 8. They read cleaner than the manual check, and they pick up the parameter name automatically via CallerArgumentExpression.

The helper formats the message for you, names the parameter correctly, and you get a one-line guard at the top of the method. The same payment-related logic looks like this:

The throw site is where the exception's stack trace begins. The runtime walks the current call stack at the moment throw executes and records each frame in ex.StackTrace. That recording is the most important debugging artifact you get from an exception, so the rest of this lesson is really about not destroying it.

Rethrowing With throw;

The most useful thing a catch block can do, besides handling the exception cleanly, is to let it keep going while doing some work along the way. Logging the failure, releasing a half-acquired resource, adding context to the exception, and then letting the original exception continue propagating is a common shape. The way to "let it keep going" is the bare throw; statement.

throw; is only legal inside a catch block. It rethrows the exception that is currently being handled, and crucially it preserves the original stack trace. The exception keeps its full history: where it was first thrown, what intermediate methods were on the stack at that moment, and now also the rethrow point.

Notice the stack trace. The original throw site (StockService.GetAvailableStock, line 12) is at the top, and OrderService.ReserveStock appears twice: once on the call that invoked GetAvailableStock, and once on the rethrow line. The runtime records both because the exception genuinely passed through that method on the way up. The first line shows where the exception came from, which is the part you need when reading a bug report.

A bare throw; is the right tool whenever the catch block exists to add side effects (logging, metrics, cleanup) but does not actually know how to recover from the exception. It says "I saw this, I did my part, now keep going."

Here is a cleanup example. A cart-validation step opens some kind of resource, hits a problem, releases the resource in the catch, and then rethrows so the caller still learns the operation failed.

A using statement or finally block would do that cleanup more idiomatically, and the _finally Block_ lesson already covered that path. The point here is the semantics of the bare throw;: the catch block did some recovery work, then handed the exception back unchanged.

A useful mental model is that throw; is the exception equivalent of returning void from a pass-through method. The method participated, but it did not change the answer.

Why throw ex; Is Wrong

The bug looks tiny. You catch an exception, you want to rethrow it, you type the obvious thing: throw ex;. The program compiles, the exception still propagates, the catch above still fires. Everything works, except the stack trace now lies about where the exception came from.

throw ex; is not a rethrow. It is a brand new throw operation that happens to use an existing exception object. The runtime sees a throw statement and resets the captured stack trace to begin at this line. The original throw site is gone, and the only frame at the top of the trace is the line in your catch block.

StockService.GetAvailableStock is no longer on the stack trace. The frame that actually threw the original exception has vanished. Anyone reading the bug report would conclude that OrderService.ReserveStock is where the trouble started, and they would waste time staring at that method instead of at the real culprit one frame up.

The difference between the two forms is hard to remember by reading the syntax, so a diagram of what ends up in the trace makes it concrete.

The exception object is identical in both branches. The only thing that changes is what ex.StackTrace says when the outer catch finally reads it. throw; keeps the full history, throw ex; keeps only the slice from this catch onward.

The rule is short: if you want to rethrow, write throw;. If you want to throw a new exception that wraps the old one, write throw new SomeException("...", ex); (the inner-exception form, covered properly in the _Inner Exceptions_ lesson). Never write throw ex; for the exception you just caught.

Some analyzers will warn about this. The Roslyn rule CA2200 ("Rethrow to preserve stack details") flags throw ex; in any catch block where ex is the caught exception. Turn it on in your project if it is not already on.

The fix is to drop ex:

A subtle but important detail: the antipattern only applies to throwing the exception you caught. Writing throw new MyException("context", ex) is a different operation. That throws a new exception, sets the original one as the InnerException, and starts a fresh stack trace beginning at the new throw site. That is a deliberate choice when you want to add domain context, and the original exception is preserved in the inner-exception chain. The _Inner Exceptions_ lesson covers that in depth.

Throw Expressions

Before C# 7, throw was strictly a statement. You could not write it anywhere an expression was expected, so guarding a value with ?? and falling through to a throw required a multi-line if-block. C# 7 changed that: throw can now appear as an expression in a handful of contexts, which lets short validation read in a single line.

The two most common spots are the right-hand side of ?? and the arms of a conditional expression (?:). Both have a natural "value or failure" shape that benefits from compact syntax.

The expression rawId ?? throw new ArgumentNullException(nameof(rawId)) reads naturally: take rawId if it is non-null, otherwise throw. It compiles down to the same IL as a regular if (rawId is null) throw ... followed by an assignment, so there is no performance difference; the form is purely for readability.

The conditional operator works the same way. One arm can return a value, the other can throw, and the type of the whole expression is the type of the value arm.

Switch expressions are a third place this shows up, and they are arguably where throw expressions earn their keep the most. A switch expression must produce a value, so a "no other case is valid" arm has to either return a sentinel or throw, and throwing is almost always the right call.

The _ => throw ... arm gives the switch expression a way to be exhaustive even when the enum gets a new member you forgot to handle. Without it, the compiler warns about a missing case and at runtime you would get a different, less helpful exception.

Expression-bodied members make this even tighter. A getter that should never return null can express that intent in one line:

Throw expressions are not allowed everywhere. They are valid in ??, conditional ?:, switch expression arms, expression-bodied members, and lambda bodies that are themselves expressions. They are not valid as standalone expressions in the middle of arithmetic, for example, because a throw does not produce a value of any usable type. The compiler will reject int x = 1 + throw new Exception(); and similar nonsense.

Wrapping Versus Rethrowing

A short detour before the next section. Sometimes you want neither a bare throw; nor a throw ex;. You want to wrap the original exception in a new one that adds domain context, while preserving the original underneath for forensic value. That is the three-argument constructor pattern: throw new SomeException("context message", innerException);.

The outer catch sees an OrderProcessingException with the domain phrasing, and the original InvalidOperationException is reachable through ex.InnerException. The full chain is also accessible by walking InnerException repeatedly, which is what ex.ToString() does for you automatically.

This is mentioned here only so you know it is a third option and not to confuse it with the antipattern. The deep dive on chains, walking InnerException, and AggregateException is the job of the _Inner Exceptions_ lesson.

ExceptionDispatchInfo for Cross-Boundary Rethrow

The bare throw; only works inside the actual catch block where the exception is live. As soon as you leave the catch (return a value, await something, put the exception in a field for later), the language stops letting you write throw;. If you store the exception object and want to rethrow it later, the obvious move is throw ex;, and you already know what that does to the stack trace.

System.Runtime.ExceptionServices.ExceptionDispatchInfo exists for exactly this situation. You capture the exception together with its current stack trace into a small object, carry it across boundaries, and later call .Throw() to rethrow it without resetting the trace.

The stack trace tells the whole story. The top of the trace is still StockService.GetAvailableStock (the original origin), the next frame is TryReserveStock (the capture site), and then there is a separator line saying "End of stack trace from previous location" before the trace continues into Flush and Main. That separator is the runtime telling you "the exception was captured here and resumed there." Both halves of its history are preserved.

The same pattern is what await uses behind the scenes. When an awaited task faulted, the framework stored the exception with ExceptionDispatchInfo.Capture and rethrew it on the continuation thread via .Throw(). That is why an exception that originally happened on a thread pool worker still looks correct in the user's await stack trace, with the "End of stack trace from previous location" marker.

A typical user-facing place for this is a fan-out helper that collects exceptions from many tasks and chooses how to surface them.

The coordinator runs every validator, captures the first failure, and after the loop rethrows it through ExceptionDispatchInfo. The stack trace still points at the actual failing lambda, not at the line that called .Throw(). If the code had instead stored the raw Exception and used throw ex; later, you would have lost the original frame entirely.

A lifecycle diagram helps anchor the steps for a rethrown exception, whether the rethrow is a bare throw; or a deferred ExceptionDispatchInfo.Throw().

There is a small footgun worth knowing: ExceptionDispatchInfo.Throw() always throws. Even though it is declared as returning void, the compiler does not know it never returns, which can cause the same kinds of definite-assignment warnings that a regular helper method would. The next section covers the fix.

The [DoesNotReturn] Attribute

Refactoring repeated throws into a small helper is something every codebase wants to do eventually. Three or four methods all guard against the same condition with the same exception, you pull the throw into a private static void helper, and you stop repeating yourself. The problem is that the C# compiler's flow analysis does not know your helper never returns, so it still expects the caller to assign return values or initialize variables on all paths.

The compiler does not realize ThrowItemNotInCart always throws. As far as it can see, the method has a void return type and could just return cleanly, in which case the price.Value line on the next iteration would dereference a null value. To make the compiler stop guessing, decorate the helper with [DoesNotReturn] from System.Diagnostics.CodeAnalysis.

[DoesNotReturn] is a flow-analysis hint. It tells the compiler "any path that calls this method ends here." Three things change for the caller after applying it:

  • Nullable-reference-type analysis prunes the "what if it returned" branch. price.Value after the guard no longer warns about price being possibly null.
  • Definite-assignment analysis stops complaining about variables that are only assigned on the "happy" branch.
  • IDE features like reachability hints draw the lines after a call to the helper as unreachable when control flow dictates.

The attribute does not change runtime behavior. It is purely metadata for the compiler and analyzers. A method tagged [DoesNotReturn] that accidentally returns at runtime will not crash the runtime, but it would mislead the static analysis, so the contract is that the method really must never return normally.

The same attribute belongs on ExceptionDispatchInfo.Throw()-based helpers and on any little utility that exists to centralize an exception throw.

Without the attribute, callers of Rethrow would get warnings about unreachable variables; with it, the compiler treats every call site as a control-flow dead end.

A common matching attribute worth knowing is [DoesNotReturnIf], also in System.Diagnostics.CodeAnalysis. It says "if this bool argument has this value, the method does not return." That is the right tool for an assertion helper, which only throws on false.

The compiler now reasons: "if IsTrue is called with false, the method does not return; the only way control reaches the next line is if the condition was true." That is enough to silence the nullability warning on userId.Value.

What You Cannot Throw

The throw statement has a few limits beyond "must be inside a catch for throw;." A handful of misuses produce compile errors or runtime exceptions you should recognize.

The first one is throwing null. From C# 8 onward, with nullable annotations on or off, the compiler rejects an obvious null throw at the source.

The compiler emits warning CS8597 ("Thrown value may be null") in a nullable-aware context. Even without that warning, the runtime treats a null throw specially: it throws NullReferenceException instead of trying to use the null as an exception object, because there is nothing else it can do. The fix is to make sure the value is non-null before throwing.

The second limit is throw; outside a catch. The bare throw; form is only legal inside a catch block, because it needs a "current exception" to rethrow. Anywhere else, the compiler rejects it.

The fix is either to throw a new exception (throw new SomeException(...)) or to move the statement inside a catch where there is something to rethrow.

The third subtlety is that the type passed to throw must derive from System.Exception. C# does not let you throw arbitrary objects, integers, or strings. The compiler enforces this at the call site.

There is a historical reason to mention this. The CLR itself does support throwing non-Exception types because some other .NET languages emit them, and starting in .NET 2.0 any non-Exception throw from elsewhere is wrapped in a RuntimeWrappedException when it reaches C# code. In practice you never see this. C# itself only throws Exception-derived types, and you should too.

Constructors throwing is fine, and it is the right move when an object cannot be constructed in a valid state. Property getters and setters throwing is also legal, but should be reserved for genuine error conditions, not for ordinary input validation, because callers rarely expect a simple property read to throw. The same goes for setters: throw on an out-of-range value, but do not throw on the cosmetic shape of the value if you can normalize it.

Throwing from the constructor is exactly right here: the object cannot be valid, so it must not exist. The alternative (allowing a half-formed instance and reporting "isValid" somewhere) leaks invalid state into the rest of the program.

Summary

  • throw new SomeException(...) raises an exception. The runtime captures a stack trace at the throw site, which is the most important debugging artifact you get from a failure.
  • A bare throw; inside a catch rethrows the current exception and preserves the original stack trace, appending the rethrow frame. It is the right tool for "log, clean up, let it propagate."
  • throw ex; is an antipattern. It resets the stack trace to begin at the rethrow line, hiding the original throw site and misleading anyone debugging the failure. Roslyn analyzer CA2200 flags this.
  • Throw expressions (C# 7+) let throw appear on the right of ??, in ?: conditionals, in switch expression arms, and in expression-bodied members, which makes compact guards and exhaustive switches read cleanly.
  • ExceptionDispatchInfo.Capture(ex) plus .Throw() preserves a stack trace across asynchronous or cross-context boundaries. The async/await machinery uses it under the hood, which is why await exceptions still point at the original throw site.
  • [DoesNotReturn] (and [DoesNotReturnIf]) on a throw-only helper tells the compiler's flow analysis that the method never returns, which removes spurious warnings about nullable returns and unassigned locals at the call site.
  • Throwing null produces NullReferenceException at runtime and is flagged with CS8597 in nullable-aware code. The thrown value must derive from System.Exception; throwing an int or string is a compile error in C#.
  • Throwing from a constructor enforces invariants: a malformed object cannot exist. Throwing from a property is legal but rare in practice and should be reserved for genuine state errors.