Перейти к содержимому

Ошибки в своём коде - результат вместо исключения, журнал, показ

Обрабатываем ошибки в прикладном коде: объект результата вместо исключения, коллекция ошибок с кодами, границы перехвата и понятный текст посетителю.

Что нужно знать заранее

В платформе принято возвращать объект результата, а не бросать исключение. Штатные методы записи данных отвечают именно так, и свой код удобнее писать в той же манере: вызывающий сам решает, что делать с отказом.

Исключения оставляют для того, что чинить некому: недоступная база, испорченная конфигурация, отсутствующий модуль. Ожидаемый отказ вроде занятого номера или пустого поля исключением не является.

Ошибку описывают сразу двумя вещами: понятным человеку текстом и стабильным кодом. Код нужен вызывающему коду для ветвления, а текст - человеку, и путать их местами не стоит: посетителю системный код ни о чём не говорит.

Шаги

  1. Решить для каждой операции заранее, ожидаемый это отказ логики или сбой окружения.
  2. Возвращать объект результата с ошибками для всех ожидаемых отказов бизнес-логики проекта.
  3. Перехватывать исключения только на границе приложения, а не внутри каждого прикладного метода.
  4. Писать в журнал техническую сторону ошибки вместе с контекстом выполняемой операции.
  5. Показывать человеку понятный текст, а не системное сообщение об ошибке с путями.

Решение

Возвращаем результат вместо исключения:

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 => 'Не получилось. Попробуйте позже или напишите нам',
};
// системный текст уходит в журнал, человеку показывают человеческий

Разделение текстов - не формальность, а вежливость. Системное сообщение пугает посетителя и ничего ему не объясняет, а поддержке нужен как раз технический текст с кодом и контекстом.

Типичные проблемы

Метод сообщил об отказе, а работа пошла дальше.

Возвращённый объект результата никто не проверил на успех выполненной операции. Проверку результата делают сразу после вызова метода, ещё до использования его данных.

В журнале нет ничего, хотя ошибка была.

Исключение перехвачено и заглушено совершенно пустым блоком обработки без записи. Перехват без записи в журнал прячет исходную причину сбоя от разработчика.

Посетитель видит системное сообщение с путём к файлу.

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

Форма показывает ошибки по одной.

Проверка формы прерывается на первом же неверно заполненном её поле. Ошибки собирают в коллекцию и показывают разом, чтобы не мучить посетителя повторными отправками.

Причина ошибки теряется при разборе.

Исключение перехвачено глубоко внутри кода и заменено там на своё сообщение. Перехват делают на границе приложения и обязательно сохраняют исходный контекст сбоя.

Частые вопросы

Когда бросать исключение, а когда возвращать результат?

Исключение - для сбоев окружения, которые прикладной код исправить не может. Ожидаемые отказы бизнес-логики возвращают объектом результата.

Зачем ошибке код, если есть текст?

Код нужен коду: по нему ветвят логику и подбирают текст для интерфейса. Текст меняется от версии к версии, а код остаётся стабильным.

Как показывать ошибки в форме?

Возвращать коллекцию ошибок и выводить их рядом с полями. Так посетитель видит все проблемы сразу, а не по одной на каждой отправке.

Что писать в журнал?

Сообщение, код, контекст операции и идентификаторы записей. Стек вызовов полезен, но его хватает и в укороченном виде.

Стоит ли заводить свои классы ошибок?

В большом решении - да, для типовых случаев вроде отсутствия прав или недоступности сервиса. В небольшом проекте достаточно кода в обычной ошибке.

Смежное

Первоисточник