Throwing Exceptions
Choose the right built-in exception type, use the argument guard helpers, and know why throw; keeps the stack trace that throw ex; destroys.
public static void ThrowIfNull(object? argument, string? paramName = null)Throwing is how a method refuses. It says: the request was impossible, so no value is coming back, and the caller must decide what to do. A method that validates its own arguments and throws early is easier to debug than one that quietly carries on with nonsense.
Pick the type that fits
| Type | Use it when |
|---|---|
ArgumentNullException | A required argument is null. |
ArgumentException | An argument is present but wrong. |
ArgumentOutOfRangeException | An argument is outside the allowed range. |
InvalidOperationException | The call is wrong for the object's current state. |
NotSupportedException | This type does not offer the operation at all. |
Never throw Exception itself: a caller cannot catch it without catching everything. Never throw NullReferenceException or IndexOutOfRangeException either, because the runtime uses those to report bugs.
Guard helpers
The argument checks are shorthand methods on the exception types. ArgumentNullException.ThrowIfNull(value), added in .NET 6, is the same as if (value is null) throw new ArgumentNullException(nameof(value));. ArgumentException.ThrowIfNullOrEmpty arrived in .NET 7, and ArgumentOutOfRangeException.ThrowIfNegative in .NET 8. You do not pass the parameter name: the compiler fills in the text of the argument expression for you.
What trips people up
Inside a catch block, throw; rethrows the same exception and keeps its stack trace. throw ex; throws the same object but resets the trace to that line, so the method that really failed disappears from the report.
Example
try
{
Orders.LoadAll(null);
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"caught for parameter: {ex.ParamName}");
Console.WriteLine($"message: {ex.Message}");
}
static class Orders
{
public static void Load(string? path)
{
// Same as: if (path is null) throw new ArgumentNullException(nameof(path));
ArgumentNullException.ThrowIfNull(path);
Console.WriteLine($"loading {path}");
}
public static void LoadAll(string? path)
{
try
{
Load(path);
}
catch (ArgumentNullException)
{
Console.WriteLine("logging the failure");
// throw; rethrows the same object and keeps the trace from Load.
// throw ex; here would restart the trace and hide Load entirely.
throw;
}
}
}
// Output:
// logging the failure
// caught for parameter: path
// message: Value cannot be null. (Parameter 'path')When to use it
- Rejecting a null or empty id at the top of a public method instead of failing deep inside it.
- Adding context to a failure in a log and letting the caller still see the original exception.
- Signalling that a method was called on an object that has not been initialised yet.
More examples
Guard clauses, long and short
Guard clauses belong at the very top of a method, before any work has started, so a bad call fails before it changes anything. The helpers take an optional paramName, but leave it out: the compiler supplies the argument text, so renaming the variable keeps the message correct.
static class Basket
{
public static int AddItem(string? sku, int quantity)
{
// Shorthand. Each helper is one if-statement and one throw.
ArgumentException.ThrowIfNullOrEmpty(sku); // .NET 7
ArgumentOutOfRangeException.ThrowIfNegative(quantity); // .NET 8
// The longhand of the second line, which is what it compiles down to:
// if (quantity < 0)
// {
// throw new ArgumentOutOfRangeException(nameof(quantity), quantity,
// "quantity must not be negative");
// }
return quantity;
}
}throw as an expression
throw is usually a statement, but it can also sit on the right of ??, inside a conditional expression, or as the whole body of an expression-bodied member. Use it where a value is required and there is nothing sensible to fall back to.
public sealed class Invoice
{
private readonly string _reference;
public Invoice(string? reference)
{
// ?? means "use the left side unless it is null". With throw on the
// right, it assigns the value or throws. The longhand is:
// ArgumentNullException.ThrowIfNull(reference);
// _reference = reference;
_reference = reference ?? throw new ArgumentNullException(nameof(reference));
}
public string Reference => _reference;
}What throw ex; costs you
Both versions throw the same exception with the same message, so a test asserting on the type still passes. The difference only shows up in a log, weeks later, when the one frame you needed is missing. Treat throw ex; as a typo for throw;.
# After `throw;` in LoadAll - the frame that failed is still listed
System.ArgumentNullException: Value cannot be null. (Parameter 'path')
at Orders.Load(String path) in /app/Orders.cs:line 9
at Orders.LoadAll(String path) in /app/Orders.cs:line 17
at Program.<Main>$(String[] args) in /app/Program.cs:line 3
# After `throw ex;` in LoadAll - Orders.Load has vanished
System.ArgumentNullException: Value cannot be null. (Parameter 'path')
at Orders.LoadAll(String path) in /app/Orders.cs:line 17
at Program.<Main>$(String[] args) in /app/Program.cs:line 3
Discussion