Ошибки в своём коде - результат вместо исключения, журнал, показ
Обрабатываем ошибки в прикладном коде: объект результата вместо исключения, коллекция ошибок с кодами, границы перехвата и понятный текст посетителю.
Что нужно знать заранее
В платформе принято возвращать объект результата, а не бросать исключение. Штатные методы записи данных отвечают именно так, и свой код удобнее писать в той же манере: вызывающий сам решает, что делать с отказом.
Исключения оставляют для того, что чинить некому: недоступная база, испорченная конфигурация, отсутствующий модуль. Ожидаемый отказ вроде занятого номера или пустого поля исключением не является.
Ошибку описывают сразу двумя вещами: понятным человеку текстом и стабильным кодом. Код нужен вызывающему коду для ветвления, а текст - человеку, и путать их местами не стоит: посетителю системный код ни о чём не говорит.
Шаги
- Решить для каждой операции заранее, ожидаемый это отказ логики или сбой окружения.
- Возвращать объект результата с ошибками для всех ожидаемых отказов бизнес-логики проекта.
- Перехватывать исключения только на границе приложения, а не внутри каждого прикладного метода.
- Писать в журнал техническую сторону ошибки вместе с контекстом выполняемой операции.
- Показывать человеку понятный текст, а не системное сообщение об ошибке с путями.
Решение
Возвращаем результат вместо исключения:
use Bitrix\Main\{Result, Error};
public function reserve(int $productId, int $quantity): Result{ $result = new Result(); if ($quantity <= 0) { return $result->addError(new Error('Количество должно быть больше нуля', 'BAD_QUANTITY')); } return $result->setData(['reserved' => $quantity]); // данные результата рядом с ошибками}Результат несёт и данные, и ошибки одновременно. Вызывающий код проверяет успех одной строкой, а при отказе получает и текст для человека, и код для собственного ветвления.
Проверяем результат у вызывающего:
$result = $service->reserve($productId, $quantity);if (!$result->isSuccess()) { foreach ($result->getErrors() as $error) { printf("%s: %s\n", $error->getCode(), $error->getMessage()); }}Непроверенный результат - самая частая ошибка в прикладном коде. Метод честно сообщил об отказе, но никто не посмотрел, и на витрине появляется заказ без резерва или пустая карточка без объяснений.
Собираем несколько ошибок сразу:
$result = new Result();if ($phone === '') { $result->addError(new Error('Укажите телефон', 'PHONE_EMPTY')); }if ($email === '') { $result->addError(new Error('Укажите почту', 'EMAIL_EMPTY')); }// форму проверяют целиком и показывают все ошибки разом, а не по однойЛовим исключения на границе приложения:
try { $result = $service->reserve($productId, $quantity);} catch (\Throwable $e) { // граница: контроллер, агент, точка входа \Bitrix\Main\Diag\Debug::writeToFile( ['msg' => $e->getMessage(), 'file' => $e->getFile()], date('H:i:s'), 'vendor.log'); $result = (new \Bitrix\Main\Result())->addError(new \Bitrix\Main\Error('Временная ошибка'));}Перехват на границе оставляет стек вызовов целым. Ловить исключение в каждом методе бессмысленно: обработать его там нечем, а исходная причина теряется по дороге наверх.
Показываем понятный текст посетителю:
$publicText = match ($error->getCode()) { 'BAD_QUANTITY' => 'Укажите количество больше нуля', 'NO_STOCK' => 'Товара не хватает на складе', default => 'Не получилось. Попробуйте позже или напишите нам',};// системный текст уходит в журнал, человеку показывают человеческийРазделение текстов - не формальность, а вежливость. Системное сообщение пугает посетителя и ничего ему не объясняет, а поддержке нужен как раз технический текст с кодом и контекстом.
Типичные проблемы
Метод сообщил об отказе, а работа пошла дальше.
Возвращённый объект результата никто не проверил на успех выполненной операции. Проверку результата делают сразу после вызова метода, ещё до использования его данных.
В журнале нет ничего, хотя ошибка была.
Исключение перехвачено и заглушено совершенно пустым блоком обработки без записи. Перехват без записи в журнал прячет исходную причину сбоя от разработчика.
Посетитель видит системное сообщение с путём к файлу.
Технический текст ошибки выводится прямо на витрину магазина обычному посетителю. Человеку показывают понятный текст, а всю техническую сторону ошибки пишут в журнал.
Форма показывает ошибки по одной.
Проверка формы прерывается на первом же неверно заполненном её поле. Ошибки собирают в коллекцию и показывают разом, чтобы не мучить посетителя повторными отправками.
Причина ошибки теряется при разборе.
Исключение перехвачено глубоко внутри кода и заменено там на своё сообщение. Перехват делают на границе приложения и обязательно сохраняют исходный контекст сбоя.
Частые вопросы
Когда бросать исключение, а когда возвращать результат?
Исключение - для сбоев окружения, которые прикладной код исправить не может. Ожидаемые отказы бизнес-логики возвращают объектом результата.
Зачем ошибке код, если есть текст?
Код нужен коду: по нему ветвят логику и подбирают текст для интерфейса. Текст меняется от версии к версии, а код остаётся стабильным.
Как показывать ошибки в форме?
Возвращать коллекцию ошибок и выводить их рядом с полями. Так посетитель видит все проблемы сразу, а не по одной на каждой отправке.
Что писать в журнал?
Сообщение, код, контекст операции и идентификаторы записей. Стек вызовов полезен, но его хватает и в укороченном виде.
Стоит ли заводить свои классы ошибок?
В большом решении - да, для типовых случаев вроде отсутствия прав или недоступности сервиса. В небольшом проекте достаточно кода в обычной ошибке.
Смежное
- События на практике - оглавление подтемы
- Логирование решения: логгер по стандарту, настройка, контекст - куда писать техническую сторону
- Свой журнал решения: файл, ротация, что писать - собственный журнал проекта
- Отладка на боевом сайте: журналы, режим ошибок, поиск виновника - как ловят ошибки в бою
- Проверка входных данных атрибутами: правила, результат, контроллер - штатная проверка с тем же результатом
- Транзакции при записи: откат, границы, что не откатится - что делать с ошибкой посреди записи
- Ядро D7 - устройство ядра целиком