Custom Exceptions
While the .NET base class library provides many general-purpose exception types, a well-designed application often benefits from custom exception classes that represent domain-specific failure conditions precisely — for example, InsufficientFundsException in a banking system or OrderAlreadyShippedException in an e-commerce order pipeline. A custom exception is simply a class deriving (directly or indirectly) from System.Exception, typically named ending in 'Exception' by convention, that can carry additional structured data about the failure beyond a plain message string, and that callers can catch selectively without accidentally swallowing unrelated errors.
Cricket analogy: Just as a scorer distinguishes a 'run out' from a 'stumped' dismissal instead of logging every wicket as generic 'out', an InsufficientFundsException names the exact failure precisely instead of a vague System.Exception.
Designing a Custom Exception Type
Microsoft's guidance recommends providing the three standard constructors — parameterless, message-only, and message-plus-inner-exception — mirroring the shape of System.Exception itself, so your type behaves consistently with the rest of the exception ecosystem and works with generic exception-wrapping code. Beyond those, it's idiomatic to add extra properties that capture structured context relevant to the failure (an account ID, an order number, a validation field name) so catching code can act on specifics instead of parsing the message string, which is fragile and not meant for programmatic use.
Cricket analogy: Giving every custom exception the same three constructors as System.Exception is like every team fielding the same eleven playing positions, so umpires and commentators can reason about any match the same way regardless of team.
public class InsufficientFundsException : Exception
{
public string AccountId { get; }
public decimal RequestedAmount { get; }
public decimal AvailableBalance { get; }
public InsufficientFundsException(
string accountId, decimal requestedAmount, decimal availableBalance)
: base($"Account {accountId} has insufficient funds: requested {requestedAmount:C}, available {availableBalance:C}.")
{
AccountId = accountId;
RequestedAmount = requestedAmount;
AvailableBalance = availableBalance;
}
public InsufficientFundsException(string message, Exception innerException)
: base(message, innerException) { }
}
public class Account(string id, decimal balance)
{
public string Id { get; } = id;
public decimal Balance { get; private set; } = balance;
public void Withdraw(decimal amount)
{
if (amount > Balance)
throw new InsufficientFundsException(Id, amount, Balance);
Balance -= amount;
}
}
try
{
new Account("ACC-100", 50m).Withdraw(200m);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"Shortfall: {ex.RequestedAmount - ex.AvailableBalance:C} on {ex.AccountId}");
}Choosing a Base Class
Deriving directly from System.Exception is usually correct for genuinely new failure categories. Occasionally it makes sense to derive from an existing, closely related built-in type — for instance, a validation exception deriving from ArgumentException when the failure really is about an invalid argument — so that generic code catching the built-in base type still handles your custom type correctly. Avoid deriving from ApplicationException, an older convention Microsoft now advises against, since it provides no meaningful behavior over Exception itself and its intended distinction from SystemException was never consistently followed across the BCL.
Cricket analogy: Deriving a validation exception from ArgumentException, rather than raw Exception, is like a specialist all-rounder still being eligible to bat in the top order, so generic top-order strategy code still handles them correctly.
Since .NET Core, the legacy binary-serialization constructor (protected Exception(SerializationInfo info, StreamingContext context)) is no longer required for most application code, because BinaryFormatter-based serialization is deprecated and disabled by default for security reasons. Only include it if you have a specific legacy serialization requirement.
Overusing custom exception types for conditions that are really just validation of expected input (e.g., a malformed email at the UI boundary) adds ceremony without benefit — reserve custom exceptions for failures that represent a genuine violation of a business rule or invariant that calling code may need to catch and react to specifically.
- Custom exceptions model domain-specific failures precisely and let callers catch them selectively.
- Provide the three conventional constructors: parameterless, message-only, and message-plus-inner-exception.
- Add strongly typed properties for structured failure context instead of relying on parsing the message string.
- Derive from System.Exception for new failure categories, or from a closely related built-in type when appropriate.
- Avoid ApplicationException — it offers no real benefit over Exception and is discouraged by Microsoft.
- Reserve custom exceptions for genuine business-rule or invariant violations, not routine input validation.
Practice what you learned
1. What is the base requirement for a class to be usable as a C# exception with throw/catch?
2. Why is it recommended to add strongly typed properties (like AccountId) to a custom exception rather than encoding all context in the message string?
3. Which base class is now discouraged by Microsoft for custom exceptions?
4. What are the three conventional constructors recommended for a well-designed custom exception?
5. When might it make sense for a custom exception to derive from ArgumentException rather than directly from Exception?
Was this page helpful?
You May Also Like
Exception Handling in C#
C# uses structured try/catch/finally blocks and a typed exception hierarchy to detect, propagate, and recover from runtime errors predictably.
using and IDisposable
Learn how .NET manages unmanaged resources through the IDisposable pattern and how the using statement and using declarations guarantee deterministic cleanup.
Inheritance in C#
Learn how C# classes derive from a single base class to reuse and extend behavior, including virtual/override methods, base member access, and sealing.
Common C# Pitfalls
A tour of the mistakes that trip up C# developers most often — from closures over loop variables to silent null reference bugs and misused async void.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics