Как правильно работать с исключениями?

Привет.

Прочитав несколько статей на Хабре, просветления по правильному использованию исключений не пришло. Скорее даже есть вопросы по организации кода, в котором используются Exceptions. Ниже приведен код с комментариями для иллюстрации вопросов. Названия классов и методов случайны и в данном случае не важны.

Итак, я вызываю метод, который может бросать исключения:

try {
	User::getById(1);
} catch (Exception $exception) {
	// Какие-то действия, например, вывести сообщение и завершить работу:
	print $exception->getMessage();
	exit;
}


Метод User::getById() в свою очередь может вызывать другие методы, которые так же могут вызывать исключения:

Class User()
{
	public function getById()
	{
		Profile::someMethod(1);
	}
}


Class Profile()
{
	public function someMethod($param)
	{
		if ($param == 1) {
			throw new Exception();
		}
	}
}


Вопрос:
Нужно ли оборачивать вызов метода someMethod() в try...catch и пробрасывать исключение дальше?
try {
	Profile::someMethod(1);
} catch (Exception $exception) {
	throw $exception;
}

или в этом нет необходимости?

Второй вопрос - продолжение первого: где бросать исключение, в вызываемом методе или вызывающем? Например, код выше можно переписать вот так:

Class Profile()
{
	public function anotherMethod($param)
	{
		if ($param == 1) {
			return false;
		}

		return true;
	}
}


Class User()
{
	public function getById()
	{
		if (Profile::anotherMethod(1) === fasle) {
			throw new Exception();
		}
	}
}


Как правильно? Или это дело вкуса?
  • Вопрос задан
  • 2266 просмотров
Пригласить эксперта
Ответы на вопрос 9
Stalker_RED
@Stalker_RED
На самом верхнем уровне вашего приложения должен быть какой-то Глобальный И Великий Обработчик Исключений Общего Назначения. Задачи у него очень простые - записать в лог что и где сломалось, показать пользователю табличку "ой, все пропало", и опционально пнуть мониторинг, отправить смс админу или письмо деду морозу.

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

Рассмотрим пару примеров:
1. Представим, что модуль "генератор превьюшек для фоточек", который верой и правдой трудился многие месяцы, однажды наталкивается на непреодолимую для него преграду и бросает исключение - "капец, место на диске кончилось, я так больше не работаю!"

Если ваш код не может ничего с этим поделать - вы пропускаете это исключение выше, пускай Глобальный И Великий Обработчик показывает юзеру красивую табличку "извините, у нас перерыв обед", и шлет письма админу в три часа ночи. А если у вас облачный хостинг, например, и вы можете на лету подключить больше дискового пространства, или может снести какой-то ненужный кэш освободив место, то можно перехватить это исключение, показать юзеру "извините, за задержку, ваши фоточки будут обработаны через пару минут", а админу прислать не красный алерт, а желтый.

2. Виджет, который показывает самых рейтинговых котиков с ютуба не смог подгрузить очередную порцию котиков, т.к. ютуб забанен роскомнадзором. Если ваш код ничего не может с этим поделать - Глобальный И Великий Обработчик Исключений покажет пользователю "ой все". Или вы перехватите это событие на более низком уровне и покажете табличку "свежих котиков нет, но вот есть рейтинг за прошлую пятницу". Или, если котики должны быть обязательно свежие, то можете предусмотреть экстренное подключение через vpn или tor, или может возьмете котиков с vimeo вместо ютуба, например.

В итоге, общие правила такие:
1. пропускаете наверх все исключения, которые не можете обработать.
2. обрабатываете те, которые вот прям обязательно нужно и важно обработать, и у вас действительно есть для этого возможность.

Важно в этом не переусердствовать, и не основывать бизнес-логику на исключениях. Этот механизм для редких, исключительных ситуаций, которые не получается обработать другим способом. Если модуль ресайза фоточек написан индусами, и его, внезапно, нечем заменить. Но если есть возможность (и необходимость) перевести эту проблему из разряда исключительной в обычную рабочую ситуацию - лучше так и сделать. Если у вас место кончается два раза в неделю - лучше настроить какой-то мониторинг, который будет следить за этим показателем и заранее разруливать такие ситуации. В случае с недоступностью сервера можно на уровне бизнес логики проверять доступность и подключать резервные каналы или источники данных, вместо того, чтобы падать с ошибкой "ой 500!".
Ответ написан
@cicatrix
было бы большой ошибкой думать
Если б однозначный ответ на ваш вопрос существовал, он бы был включён в гайды по программированию.
Существует очень много факторов, влияющих на то, как вы должны поступать с исключениями:
1) Вообще их не ловить, пусть об этом заботится вызывающий
2) Ловить и обрабатывать определённые исключения, остальные пробрасывать
3) Ловить, генерить своё, обёртывать в него внутреннее и бросать своё
4) Ловить всё, обрабатывать самостоятельно
5) Душить всё

(и это далеко не полный список).

Определять это должна архитектура приложения и места данного конкретного куска кода в этой архитектуре.
В целом, всегда следует подходить к коду с точки зрения удобства вызывающего. Если вызывающему (коду, пользователю, программе, сервису, приложению, и т. д.) нужен только результат и не важно, что там у вас случилось, то обрабатывать надо самому. Если вызывающему нужен полный контроль - то можно вообще не обрабатывать. Всегда есть какой-то разумный баланс между двумя крайностями, и каждый раз надо решать отдельно. Универсального решения здесь нет.

В крупном проекте есть руководитель/архитектор, который должен задать либо общие правила, либо правила конкретно для вас. Для open source вопрос сложный - в библиотеках обычно исключения не обрабатываются, иногда пробрасываются.
Если вы контролируете код от метода MyMethod и дальше вниз по иерархии вызовов, то решать вообще-то вам. Как вам удобнее, так и поступайте. Если внизу по иерархии другой код, то обычно, пока явно не оговорено обратное, поступать надо так же, как поступает код "снизу" - если бросает/пробрасывает, то и вы делайте так же, если нет - соответственно, и вы обрабатываете.
Ответ написан
Комментировать
try...catch не нужно использовать как управляющую конструкцию или для проверки состояния вместо if...else. Она для обработки транзакций. Если потенциально транзакция может не завершится, бросается эксепшн. Например запись в базу.
try {  
  пишем в базу 100 записей
} catch (Exception $e) {
  если на 99й записи сбой — откат
  echo $e->getMessage();
}
Ответ написан
Комментировать
@Stqs
senior software developer
по-моему, тут вообще проблема заключается в том что бросать исключение будет не правильно
особенно во втором примере
исключение оно для обработки исключительных ситуаций (c) КО
у вас же исключение происходит(вы сами его бросаете) для передачи управления другому коду
то есть для организации логики
это как бы атата, называется антипаттерном и за это можно на код-ревью получить по жопе

Нужно ли оборачивать вызов метода someMethod() в try...catch и пробрасывать исключение дальше?

а какой в этом смысл?
вы таким образом можете себе навредить испортив оригинальный трейсбек
Ответ написан
Комментировать
qonand
@qonand
Software Engineer
Что такое исключение? это грубо говоря оповещение о том что что-то пошло не так и программный код не может полноценно отработать. Соответственно и выбрасываться оно должно в коде где произошла проблема - т.е. в вызываемом методе, а уже вызывающий код должен как-то на нее реагировать - перелавливать с помощью try-catch и что-то делать (например как-то пытаться исправить ситуацию) либо пробрасывать ее дальше...
Ответ написан
Комментировать
egor_nullptr
@egor_nullptr
Вот тут всё подробно описано https://isocpp.org/wiki/faq/exceptions
Ответ написан
Комментировать
DmitriyEntelis
@DmitriyEntelis
Думаю за деньги
Моя личная точка зрения:

Нужно ли оборачивать вызов метода someMethod() в try...catch и пробрасывать исключение дальше?

Общее правило: если у вас есть какая-то своя особая логика на случай падения в этом конкретном месте - то да, нужно.
Если конкретной логики нет - то соответственно нет.

Второй вопрос - продолжение первого: где бросать исключение, в вызываемом методе или вызывающем?

Там где по смыслу проверяется бизнес-логика.
Переходя к более конкретным примерам - функция getUser может кидать исключение если пользователь не найден, функция getAdminUser использующая функцию getUser может дополнительно к этому кидать исключение если найденный пользователь не является админом.
Ответ написан
Комментировать
AlexanderYudakov
@AlexanderYudakov
C#, 1С, Android, TypeScript
Нужно ли оборачивать вызов метода someMethod() в try...catch и пробрасывать исключение дальше?
try {
  Profile::someMethod(1);
} catch (Exception $exception) {
  throw $exception;
}

Это равносильно простому вызову метода:
Profile::someMethod(1);

Второй вопрос - продолжение первого: где бросать исключение, в вызываемом методе или вызывающем?

Исключение нужно выбрасывать как можно раньше — в том месте, где проблема обнаружена:
Class Profile()
{
  public function anotherMethod($param)
  {
    if ($param == 1) {
      throw new Exception('$param не может быть 1');
    }
  }
}

Это позволит в вызывающем коде в большинстве случаев вообще ничего не писать. В этом и есть суть исключений: чем больше у нас проект, тем длиннее стек вызовов, и тем больше программного кода у нас находится в середине стека и написано очень просто:
Class User()
{
  public function getById()
  {
    Profile::anotherMethod(1);
  }
}

Перехват исключений через try... catch требуется только в двух случаях:
1) в вершине стека - там где наш язык программирования заканчивается и нам нужно дальше вызывающей стороне сообщить о возникшей проблеме — правильно вы написали:

try {
  User::getById(1);
} catch (Exception $exception) {
  // Какие-то действия, например, вывести сообщение и завершить работу:
  print $exception->getMessage();
  exit;
}

2) в редких случаях в середине стека - когда мы знаем, как решить возникшую проблему и при этом успешно завершить выполнение своей собственной задачи.
Ответ написан
Комментировать
@maxtm
Make money, not job
Не ловите исключения.

Ваш код должен делать это только в сугубо-редких случаях, в остальных - все исключения ловятся в одном месте.
Это избавит Вас от проблем логирования ошибок (это сделано в едином месте), откате транзакций, возврата ответа с кодом ошибки прочего.

Сделайте единую точку входа в апку, в ней отлавливайте все исключения, логируйте.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы