100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Custom Exceptions

Custom exception types let you model domain-specific failure conditions precisely, carrying extra context and enabling callers to catch them selectively.

Exception Handling & MemoryIntermediate8 min readJul 9, 2026
Analogies

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.

csharp
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

Was this page helpful?

Topics covered

#CStudyNotes#Programming#CustomExceptions#Custom#Exceptions#Designing#Exception#ErrorHandling#StudyNotes#SkillVeris