1. Если это команда, не возвращайте результат, просто выдайте исключение в случае сбоя (это также связано с CQRS, потому что вы не можете сказать, успешно ли выполнена команда, возвращая значение)
  2. Исключение должно быть индивидуальным и подробным, не используйте базовые исключения, это усложняет отладку.
  3. Выбрасывать исключение как можно быстрее (вроде раннего возврата)
  4. Не забудьте перехватить последнее самое общее исключение, если фреймворк не обработает его за вас. Симфони делает.
  5. Не полагайтесь на исключения сторонних пакетов. Отловить все 3-е исключения и бросить свое
  6. Дочерний класс не должен генерировать новые типы исключений из-за LSP.
  7. Логика не должна прятаться в конструкции catch. Используйте ранний возврат
  8. Поместите исключения в папку, где они используются
  9. RuntimeException — не может быть восстановлено за один ответ(неудачное соединение, все что приводит к 500),
    Exception — только логическое, типа entity not found или подобное, все 4** кода
  10. Исключения могут быть вложенными
  11. Вы можете поймать несколько исключений одновременно

На уровне данных, например сервис\провайдер

  1. Вы должны ловить только определенные исключения — те, которые вы можете обработать. Общие исключения будут перехвачены фреймворком без раскрытия какой-либо критической информации снаружи.
  2. Убедитесь, что вы распространяете предыдущий (заверните один в другой, как буррито)
  3. Чтобы лучше понять — погуглите «абстракция просачивается»

В контроллерах

  1. Поймать собственное исключение
  2. Создать новое исключение HTTP, используя исключение фреймворка
  3. Имеет смысл распространять до уровня контроллера только те исключения, которые можно обработать. Вы можете подумать о недопустимых входных данных (которые должны быть обработаны перед контроллером в объекте запроса)