Исключение в PHP выбрасывается через throw new Exception('message');. Выполнение останавливается на этой строке и переходит к ближайшему блоку catch, который принимает тип исключения, где $e->getMessage() возвращает сообщение. Используйте конкретный класс, например InvalidArgumentException, чтобы вызывающий код мог различать проблемы.
Исключение это объект. throw передаёт его вверх по стеку вызовов первому вызывающему коду с подходящим catch, и уже он решает, что делать; функция, которая обнаружила проблему, знать этого не обязана. Механика перехвата, finally и перехват нескольких типов описаны на странице try catch.
Что содержит объект исключения
Каждое исключение несёт сообщение, целочисленный код, файл и строку, где оно создано, и трассировку стека. Конструктор принимает ($message, $code, $previous), все аргументы необязательны:
getLine() это строка с throw new, а не строка с catch. Трассировка перечисляет вызовы, которые туда привели (#0 /home/index.php(8): findOrder()), и обычно её читают первой, когда ошибка появляется в логе. Приведение исключения к строке (echo $e; или (string) $e) выводит всё это в стандартном формате PHP.
Встроенные классы исключений
В PHP есть семейство классов исключений в стандартной библиотеке PHP (SPL). Если выбрасывать тот, который называет проблему, код читается лучше, а вызывающий код может перехватывать узко. Все они наследуют Exception:
| Класс | Когда выбрасывать |
|---|---|
InvalidArgumentException | аргумент имеет неверную форму: пустое имя, неизвестная опция |
DomainException | значение вне множества осмысленных: отрицательная цена |
OutOfRangeException | код запросил индекс, которого никогда не может быть (ошибка в вызывающем коде) |
OutOfBoundsException | ключа или индекса нет в данных, известных только во время выполнения |
RangeException | вычисленный результат во время выполнения вышел за допустимый диапазон |
LengthException | что-то слишком длинное или слишком короткое |
RuntimeException | проблема, которую можно обнаружить только во время выполнения: полный диск, таймаут |
UnexpectedValueException | функция вернула или получила значение неожиданного типа |
LogicException | неправилен сам код, это ошибка, а не плохие входные данные |
JsonException | выбрасывается json_encode()/json_decode() с JSON_THROW_ON_ERROR |
InvalidArgumentException, DomainException, LengthException и OutOfRangeException наследуют LogicException; OutOfBoundsException, RangeException и UnexpectedValueException наследуют RuntimeException. Родителей любого класса можно проверить самим:
Родитель TypeError это Error, а не Exception: он принадлежит второму семейству, о нём ниже.
Ошибки, которые выбрасывает сам PHP
Начиная с PHP 7, а с PHP 8 гораздо чаще, собственные функции и операторы PHP выбрасывают подклассы Error для проблем, которые раньше были предупреждениями. Перехватываются они так же, но catch (Exception $e) с ними не совпадёт:
DivisionByZeroError, TypeError, ValueError, ArgumentCountError, UnhandledMatchError и простой Error все наследуют Error. Каждая из них чаще отмечает ошибку в вызывающем коде, чем плохие данные, поэтому они живут вне дерева Exception. Именно declare(strict_types=1) делает str_repeat(5, 2) ошибкой TypeError; без него PHP привёл бы 5 к "5".
Собственный класс исключения
Часто хватает подкласса в одну строку: смысл несёт само имя класса, и вызывающий код может перехватывать именно этот тип. Когда перехватчику нужны данные (какой заказ, какая сумма), добавьте свойства и передайте сообщение наверх через parent::__construct():
Родителя выбирайте внимательно: наследование RuntimeException (а не простого Exception) означает, что код, перехватывающий RuntimeException, обработает и ваше исключение. Небольшая иерархия, одно базовое исключение на библиотеку или модуль с конкретными подклассами под ним, позволяет вызывающему коду выбирать между "перехватить всё из платежей" и "перехватить только этот случай".
Цепочки исключений через previous
Когда вы перехватываете низкоуровневое исключение и выбрасываете более высокоуровневое, передайте исходное третьим аргументом. Ничего не теряется: getPrevious() ведёт назад к первопричине.
Пользователь видит "customers.csv could not be imported"; лог, пройдя по цепочке, получает ещё и "row 2 has 2 columns". Если исключение из цепочки так и не перехвачено, фатальная ошибка PHP выводит всю цепочку: сначала исходную причину, затем каждую обёртку под строкой Next.
throw как выражение
Начиная с PHP 8.0 throw это выражение, поэтому он может стоять везде, где ожидается значение: после ??, в тернарном операторе, в стрелочной функции. С ним "получи это или упади" записывается одной строкой:
Когда не нужно выбрасывать исключение
Исключения нужны для ситуаций, с которыми текущая функция не может справиться и которых не ожидает обычный ход работы. Поиск, который ничего не нашёл, пустое необязательное поле или пользователь, который не вошёл в систему, это обычные исходы: возвращайте null, false или пустой массив и дайте вызывающему коду проверить, а исключения оставьте для случаев, когда продолжать было бы неправильно.
Ещё две привычки делают код с исключениями читаемым: никогда не перехватывайте исключение лишь для того, чтобы его проигнорировать (пустой catch (Exception $e) {} скрывает следующую настоящую ошибку), и выбрасывайте конкретные классы, а не голый Exception, чтобы вызывающему коду не приходилось ловить всё ради одного случая.
Часто задаваемые вопросы
Как выбросить исключение в PHP?
Создайте объект исключения и выбросьте его: throw new InvalidArgumentException('Quantity must be positive');. Выполнение останавливается на этой строке и переходит к ближайшему подходящему блоку catch; если такого нет, скрипт завершается фатальной ошибкой "Uncaught".
Как создать собственное исключение в PHP?
Унаследуйте Exception или один из его подклассов: class PaymentFailedException extends RuntimeException {}. Одной этой строки достаточно, чтобы выбрасывать и перехватывать исключение по его собственному типу. Добавьте свойства и конструктор, когда перехватчику нужны дополнительные данные, и вызовите parent::__construct($message, $code, $previous).
Чем getMessage отличается от getCode?
getMessage() возвращает текст, переданный первым аргументом конструктора, он предназначен для людей и логов. getCode() возвращает целое число, переданное вторым аргументом (по умолчанию 0), оно полезно для машинных проверок, например для сопоставления ошибок с HTTP-кодами.
Какое исключение выбрасывать в PHP?
Используйте встроенный класс SPL, который соответствует проблеме: InvalidArgumentException для неверного аргумента, DomainException для значения вне допустимого, RuntimeException для сбоев, видимых только во время выполнения (полный диск, таймаут), LogicException для ошибок программиста. Для ошибок, которые вызывающему коду нужно различать, создайте собственный подкласс.
Что такое цепочка исключений в PHP?
Передача исходного исключения третьим аргументом конструктора, когда вы выбрасываете новое: throw new ImportException('Import failed', 0, $e);. Новое исключение даёт контекст, а его $e->getPrevious() возвращает исходную причину, так что никакие подробности не теряются.