Зачем нужно создавать свои классы ошибок (исключений)?

«Зачем нужно создавать свои классы ошибок (исключений)?» — вопрос из категории ООП, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Создание пользовательских классов исключений позволяет точно моделировать предметную область приложения и создавать четкую, понятную иерархию ошибок. Это ключевой инструмент для написания надежного и поддерживаемого кода.

Основные причины:

  1. Семантическая точность: Стандартные исключения (вроде RuntimeException или InvalidArgumentException) часто слишком общие. Свои исключения точно описывают, что пошло не так (например, PaymentDeclinedException, UserNotFoundException, InvalidOrderStateException).
  2. Гранулярная обработка: Позволяет перехватывать и обрабатывать разные типы сбоев по-разному.
  3. Добавление контекста: В кастомный класс можно добавить дополнительные свойства, которые помогут в логировании и отладке (ID транзакции, код ошибки внешнего API и т.д.).
  4. Улучшение читаемости кода: По имени класса исключения сразу понятна природа ошибки.

Пример на PHP:

<?php
// Базовое исключение для домена "Платежи"
namespace AppPaymentException;

class PaymentException extends RuntimeException {
    protected string $transactionId;

    public function __construct(string $message, string $transactionId, int $code = 0, ?Throwable $previous = null) {
        parent::__construct($message, $code, $previous);
        $this->transactionId = $transactionId;
    }

    public function getTransactionId(): string {
        return $this->transactionId;
    }
}

// Конкретные типы ошибок платежа
class InsufficientFundsException extends PaymentException {}
class CardDeclinedException extends PaymentException {}
class GatewayTimeoutException extends PaymentException {}

// Использование в коде
class PaymentProcessor {
    public function charge(Order $order): void {
        $transactionId = $this->generateId();
        // ... логика проверки баланса
        if (!$order->user->hasSufficientFunds($order->total)) {
            throw new InsufficientFundsException(
                "User does not have enough funds.",
                $transactionId
            );
        }
        // ... попытка списания
    }
}

// Обработка на более высоком уровне
try {
    $processor->charge($order);
} catch (InsufficientFundsException $e) {
    // Конкретная обработка: показать сообщение пользователю, предложить другой способ
    $this->logger->warning('Insufficient funds', ['transaction' => $e->getTransactionId()]);
    $this->redirectToPaymentMethodSelection();
} catch (CardDeclinedException $e) {
    // Другая обработка
} catch (PaymentException $e) {
    // Общая обработка всех платежных ошибок
    $this->logger->error('Payment failed', ['exception' => $e]);
    throw new HttpException(500, 'Payment processing error');
}

Такой подход делает поток ошибок в приложении управляемым и понятным для разработчиков.