[C#] How to Properly Define and Implement Custom Exceptions

While C# provides many standard exception classes like ArgumentException and InvalidOperationException, relying solely on them can sometimes make the meaning of errors ambiguous when expressing application-specific business logic errors (e.g., insufficient inventory, account locked, insufficient funds).

In such cases, defining a “Custom Exception” by inheriting from the Exception class allows you to clarify the meaning of the error and makes handling it on the caller side easier. Here, we will explain how to implement a custom exception representing “insufficient funds” using a bank account withdrawal process as an example.

目次

Rules for Defining Custom Exception Classes

.NET design guidelines recommend following these rules when creating exception classes:

  • Inheritance: Inherit from the System.Exception class.
  • Naming: Always suffix the class name with Exception (e.g., InsufficientFundsException).
  • Constructors: Implement at least the following three standard constructors:
    1. A default constructor with no arguments.
    2. A constructor that accepts an error message.
    3. A constructor that accepts an error message and an inner exception (InnerException).

Practical Code Example: Implementing Insufficient Funds Exception

The following code defines InsufficientFundsException, which is thrown when the balance is insufficient during a bank account withdrawal operation.

using System;

namespace BankingSystem
{
    // 1. Definition of custom exception class
    // Inherit from Exception to represent "Insufficient Funds" in business logic.
    public class InsufficientFundsException : Exception
    {
        // --- Recommended 3 Standard Constructors ---

        // (1) Default Constructor
        public InsufficientFundsException()
        {
        }

        // (2) Constructor accepting an error message
        public InsufficientFundsException(string message)
            : base(message)
        {
        }

        // (3) Constructor accepting a message and an inner exception (for wrapping exceptions)
        public InsufficientFundsException(string message, Exception innerException)
            : base(message, innerException)
        {
        }

        // (Optional) You can add additional properties
        // Example: The deficit amount
        public decimal DeficitAmount { get; set; }
    }

    // Account Class
    public class BankAccount
    {
        public decimal Balance { get; private set; }

        public BankAccount(decimal initialBalance)
        {
            Balance = initialBalance;
        }

        public void Withdraw(decimal amount)
        {
            if (amount > Balance)
            {
                decimal deficit = amount - Balance;
                
                // Instantiate and throw the custom exception
                // Set values to custom properties (DeficitAmount) as needed
                var ex = new InsufficientFundsException($"Insufficient funds. Current balance: {Balance:C}, Requested: {amount:C}");
                ex.DeficitAmount = deficit;
                throw ex;
            }

            Balance -= amount;
            Console.WriteLine($"Withdrawal complete: {amount:C} (Balance: {Balance:C})");
        }
    }

    class Program
    {
        static void Main()
        {
            var account = new BankAccount(1000); // Balance: 1,000

            try
            {
                // Attempt to withdraw 5,000 (Insufficient funds)
                account.Withdraw(5000);
            }
            // Explicitly catch the custom exception
            catch (InsufficientFundsException ex)
            {
                Console.WriteLine("=== Error: Transaction Failed ===");
                Console.WriteLine(ex.Message);
                Console.WriteLine($"Deficit Amount: {ex.DeficitAmount:C}");
            }
            // Catch other unexpected exceptions
            catch (Exception ex)
            {
                Console.WriteLine($"System Error: {ex.Message}");
            }
        }
    }
}

Execution Result

=== Error: Transaction Failed ===
Insufficient funds. Current balance: ¥1,000, Requested: ¥5,000
Deficit Amount: ¥4,000

(Note: The currency symbol depends on the system’s locale setting).

Technical Points

1. Importance of Standard Constructors

When creating a custom exception, you must ensure you do not break the functionality of the parent Exception class. The (string message, Exception inner) constructor is particularly important. Without it, if you wrap an exception that occurred in a lower layer with this custom exception, the InnerException chain will be broken, making it difficult to trace the root cause.

2. Leveraging Custom Properties

One of the major benefits of creating a custom exception instead of using the standard Exception class is the ability to add custom properties. In the example above, the DeficitAmount property was added. This allows the catch block to perform programmatic processing based on the deficit amount (e.g., logic to cover the deficit from another account) safely, without having to parse the error message string.

3. When to Use

You do not need to create custom exceptions for every error.

  • Use Standard Exceptions: If an argument is null, use ArgumentNullException; if an operation is invalid, use InvalidOperationException.
  • Use Custom Exceptions: Define them when it is a “specific business rule violation” and you want the caller to treat that error specially and perform recovery processing.

Summary

Defining custom exceptions clarifies the intent (semantics) of your code and organizes the structure of error handling. By implementing the three recommended constructors and adding custom data as needed, you can achieve robust and maintainable error handling.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次