Что такое пользовательское (кастомное) исключение в C# и когда его создавать?

«Что такое пользовательское (кастомное) исключение в C# и когда его создавать?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Пользовательское исключение — это класс, унаследованный от System.Exception или его производных, предназначенный для представления специфичных для предметной области ошибок.

Зачем создавать свои исключения?

  1. Семантика и ясность: throw new OrderNotFoundException(...) понятнее, чем throw new KeyNotFoundException(...).
  2. Группировка обработки: Можно ловить конкретный тип исключения (catch (PaymentFailedException ex)).
  3. Дополнительные данные: Возможность добавить свойства с контекстом ошибки (ID заказа, код ошибки API).

Правила создания:

  1. Наследоваться от Exception (или ApplicationException, но это менее распространено).
  2. Добавлять стандартные конструкторы.
  3. Помечать класс атрибутом [Serializable], если исключение может пересекать границы доменов приложения (например, в Remoting или WCF).

Пример правильно оформленного кастомного исключения:

[Serializable] // Для поддержки сериализации
public class InsufficientFundsException : Exception
{
    // Дополнительные свойства для контекста
    public string AccountId { get; }
    public decimal CurrentBalance { get; }
    public decimal RequiredAmount { get; }

    // Стандартные конструкторы
    public InsufficientFundsException() { }
    public InsufficientFundsException(string message) : base(message) { }
    public InsufficientFundsException(string message, Exception inner) : base(message, inner) { }

    // Конструктор с дополнительными данными
    public InsufficientFundsException(string accountId, decimal currentBalance, decimal requiredAmount)
        : base($"Account '{accountId}' has insufficient funds. Current: {currentBalance}, Required: {requiredAmount}")
    {
        AccountId = accountId;
        CurrentBalance = currentBalance;
        RequiredAmount = requiredAmount;
    }

    // Конструктор для десериализации (важно для [Serializable])
    protected InsufficientFundsException(
      System.Runtime.Serialization.SerializationInfo info,
      System.Runtime.Serialization.StreamingContext context) : base(info, context)
    {
        AccountId = info.GetString(nameof(AccountId));
        CurrentBalance = info.GetDecimal(nameof(CurrentBalance));
        RequiredAmount = info.GetDecimal(nameof(RequiredAmount));
    }
}

// Использование в бизнес-логике
public void ProcessWithdrawal(string accountId, decimal amount)
{
    var balance = GetBalance(accountId);
    if (balance < amount)
    {
        // Бросаем информативное исключение с контекстом
        throw new InsufficientFundsException(accountId, balance, amount);
    }
    // ... продолжение обработки
}

Рекомендация: Не создавайте новые исключения для ситуаций, которые уже точно описываются встроенными типами (ArgumentNullException, InvalidOperationException).