Catching exceptions is half of error handling; the other half is throwing the right one. A well chosen exception tells the caller exactly what went wrong and whether it was their mistake or the program's state. This page covers the throw side; try catch covers handling.
The throw statement and guard clauses
throw takes an exception object. Execution of the method stops there, and the exception looks for a handler up the call stack. The most common use is a guard clause: checks at the top of a method that reject invalid input before any work is done.
Output:
ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100
nameof(amount) produces the string "amount" and stays correct if the parameter is renamed. The argument exceptions store it in ParamName, which tools and logs use to point at the bad argument.
Guard clauses keep the rest of the method simple: past the checks, the code can assume valid input. They also fail at the point of the mistake instead of letting a bad value travel on and cause a confusing NullReferenceException three methods later.
Which exception type to throw
Reuse a built-in type when it describes the situation; callers already know how to handle it.
| Situation | Throw |
|---|---|
A required argument is null | ArgumentNullException |
| An argument is outside the allowed range (negative quantity, index past the end) | ArgumentOutOfRangeException |
| An argument is invalid in another way (empty name, malformed ID) | ArgumentException |
| The call is not valid in the object's current state | InvalidOperationException |
The operation is never supported by this type (a read-only collection's Add) | NotSupportedException |
| The method has not been written yet | NotImplementedException |
An object was used after Dispose | ObjectDisposedException |
| A timed operation ran out of time | TimeoutException |
The line between the first three rows and InvalidOperationException is who needs to change something. An argument exception says "call this differently". InvalidOperationException says "the call was fine, but not now".
Do not throw Exception, SystemException or ApplicationException directly: callers cannot catch them without also catching everything else. Do not throw NullReferenceException, IndexOutOfRangeException or StackOverflowException yourself either; the runtime reserves those for actual bugs.
Throw expressions
Before C# 7, throw was only a statement. Since C# 7 it can also appear as an expression in three places, which turns common checks into one line:
Output:
Ana <ana@example.com>
Null: name
ArgumentException: email
Note that ArgumentNullException derives from ArgumentException, so a catch (ArgumentException) placed first would also catch the null case. The order of catch clauses matters for the same reason when you handle both.
ThrowIfNull and friends (.NET 6 and later)
Modern .NET adds static helpers that write the check and the throw for you, with the parameter name captured automatically:
public void Ship(Order order, int quantity, string address)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
ArgumentException.ThrowIfNullOrWhiteSpace(address); // .NET 8
ObjectDisposedException.ThrowIf(disposed, this); // .NET 7
// ...
}
They behave the same as the hand-written if and throw, and they keep guard clauses to one line each. On older targets, write the if form shown earlier.
Writing a custom exception class
Create your own exception type when callers need to catch this specific failure separately, or when the handler needs data that a message string cannot carry well.
Output:
Cannot withdraw 25 from a balance of 15.
Short by 10
The conventions:
- The name ends in
Exception. - It derives from
Exception(or from a more specific built-in type when it is a special case of one, such asInvalidOperationException). - It has the three standard constructors: none, message, and message plus inner exception. Add your own constructors on top.
- Extra data goes in read-only properties, set in the constructor. A handler can then act on
e.Requestedinstead of parsing the message.
Wrapping with an inner exception
When a low-level failure should surface as a higher-level one, wrap it. The original is kept as InnerException, so no information is lost:
Output:
Setting 'port' must be a number, got '80a'.
Caused by: FormatException
The caller now deals in terms of configuration, which it understands, and logging e.ToString() prints the whole chain including the FormatException and its stack trace. Only wrap when you add meaning; wrapping every exception in a generic MyAppException just makes handlers dig through InnerException.
Throwing vs returning a result
Exceptions are for failures the caller does not expect in normal operation. For outcomes that are routine, such as a lookup that often finds nothing or user input that is often invalid, the .NET convention is the Try pattern: return bool and hand the value back through an out parameter.
public bool TryWithdraw(decimal amount, out string error)
{
if (amount > Balance) { error = "Insufficient funds."; return false; }
Balance -= amount;
error = null;
return true;
}
Many types offer both: int.Parse throws, int.TryParse returns false; dict[key] throws, dict.TryGetValue returns false. Throwing an exception costs far more than returning a value, so it should not sit on a path that runs thousands of times a second. See ref and out for out parameters.
Writing good messages
An exception message is read by a developer looking at a log. Make it state what was wrong and, where safe, the offending value: "Quantity must be between 1 and 99, got 0." beats "Invalid input." Write full sentences, and keep secrets such as passwords and tokens out of messages, since they end up in log files.
Common mistakes
- Throwing
Exceptionitself. Callers cannot catch it selectively; use a specific type. - Passing the message where the parameter name goes.
new ArgumentNullException("name")takes the parameter name; the message comes second. - Hard-coded parameter names. Use
nameof(param)so renames keep them correct. - Custom exceptions with no extra meaning. If a built-in type fits, use it.
- Losing the original error when wrapping. Always pass it as the inner exception.
Frequently Asked Questions
How do I throw an exception in C#?
Create an exception object and throw it: throw new ArgumentException("Amount must be positive", nameof(amount));. Execution stops at that line and the exception travels up the call stack to the nearest matching catch. Pick the most specific built-in type that describes the problem, or a custom type when callers need to handle this case separately.
How do I create a custom exception in C#?
Derive a class from Exception whose name ends in Exception, and give it the standard constructors: one with no arguments, one taking a message, and one taking a message and an inner exception, each calling the matching base(...) constructor. Add read-only properties for any data a handler needs, such as an order ID or a balance.
When should I throw ArgumentException vs InvalidOperationException?
Throw an ArgumentException (or ArgumentNullException / ArgumentOutOfRangeException) when a caller passed a bad value: the fix is to call the method differently. Throw InvalidOperationException when the arguments are fine but the object is in the wrong state for the call, such as reading from a closed connection or withdrawing from a frozen account.
What is a throw expression in C#?
Since C# 7, throw can be used as an expression in three places: after ??, as either branch of ?:, and as the body of an expression-bodied member or lambda. For example _name = name ?? throw new ArgumentNullException(nameof(name)); assigns or throws in one line.
What does ArgumentNullException.ThrowIfNull do?
It is a static helper added in .NET 6: ArgumentNullException.ThrowIfNull(customer); throws ArgumentNullException with the parameter name filled in automatically when customer is null, and does nothing otherwise. Later versions added similar helpers such as ArgumentException.ThrowIfNullOrEmpty (.NET 7) and ArgumentOutOfRangeException.ThrowIfNegative (.NET 8).