Ошибки и исключения: Error, Exception, Throwable
Вопрос, где половина кандидатов до сих пор отвечает по правилам PHP 5.
Throwable — интерфейс, который реализуют оба ствола иерархии:
Exception(ошибки времени выполнения, которые приложение может обработать) иError(ошибки движка и программиста).
Вопрос 1: «Чем Error отличается от Exception?»
Что проверяет интервьюер
Знаете ли вы, что с PHP 7 фатальные ошибки движка перестали «убивать» скрипт и стали объектами Error, которые можно поймать. Оба класса реализуют Throwable, но не наследуют друг друга: catch (Exception $e) не поймает TypeError.
Throwable (interface)
├── Error
│ ├── TypeError
│ ├── ValueError
│ ├── ArithmeticError → DivisionByZeroError
│ ├── ArgumentCountError
│ └── AssertionError
└── Exception
├── LogicException
│ ├── InvalidArgumentException
│ ├── DomainException
│ └── LengthException
└── RuntimeException
├── OutOfBoundsException
├── RangeException
└── UnexpectedValueException
<?php
function divide(int $a, int $b): float {
if ($b === 0) {
throw new InvalidArgumentException('делитель не может быть нулём');
}
return $a / $b;
}
try {
echo divide(10, 2), "\n";
echo divide(10, 0), "\n";
} catch (InvalidArgumentException $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
} finally {
echo "finally выполняется всегда\n";
}
try {
echo intdiv(1, 0);
} catch (DivisionByZeroError $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
var_dump(new InvalidArgumentException('x') instanceof Throwable);
var_dump(new TypeError('x') instanceof Exception);
Вывод:
5 InvalidArgumentException: делитель не может быть нулём finally выполняется всегда DivisionByZeroError: Division by zero bool(true) bool(false)
Последняя строка — суть вопроса: TypeError не является Exception. Если нужно поймать вообще всё (например, в глобальном обработчике перед отдачей 500-й страницы), ловят Throwable.
Практическое правило выбора
LogicExceptionи потомки — «программист ошибся»: неверный аргумент, нарушенный инвариант. Такое чинят кодом, а не обработкой.RuntimeExceptionи потомки — «внешний мир подвёл»: база недоступна, файл не читается, API вернул мусор. Это обрабатывают и логируют.Errorобычно не ловят точечно: он означает, что нарушен контракт языка.
Вопрос 2: «Зачем нужен finally и что будет с return внутри него?»
finally выполняется всегда — и при нормальном завершении блока, и при исключении, и даже при return внутри try. Это место для освобождения ресурсов: закрыть файл, откатить транзакцию, снять блокировку.
$pdo->beginTransaction();
try {
$repository->save($order);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e; // пробрасываем дальше, не проглатываем
} finally {
$lock->release(); // выполнится в любом случае
}
Важная тонкость, которую любят спрашивать: return внутри finally перезаписывает значение, возвращённое из try, и «съедает» летящее исключение. Поэтому return в finally не пишут.
Вопрос 3: «Как обрабатывать ошибки на уровне приложения?»
// одна точка входа для необработанных исключений
set_exception_handler(function (Throwable $e) use ($logger) {
$logger->error($e->getMessage(), ['exception' => $e]);
http_response_code(500);
echo 'Внутренняя ошибка';
});
// превращаем предупреждения и notice в ErrorException
set_error_handler(function (int $no, string $str, string $file, int $line): bool {
throw new ErrorException($str, 0, $no, $file, $line);
});
// ловим то, что не ловится: фатальные ошибки памяти и таймаута
register_shutdown_function(function () use ($logger) {
$err = error_get_last();
if ($err !== null && $err['type'] === E_ERROR) {
$logger->critical('Fatal: ' . $err['message']);
}
});
Что стоит добавить в ответ: на проде display_errors должен быть выключен, а log_errors — включён; текст исключения нельзя показывать пользователю (в нём бывают пути и параметры подключения); свои исключения полезно наследовать от общего AppException, чтобы отделять «свои» ошибки от чужих; а «проглатывание» через пустой catch { } — худшее, что можно сделать с ошибкой.
Типичные ошибки кандидатов
- Уверены, что
catch (Exception $e)ловит всё. Не ловитError— нуженThrowable. - Ловят исключение и логируют только
getMessage(), теряя стек-трейс иgetPrevious(). - Используют исключения как управление потоком — например, бросают их в цикле для «выхода».
- Не знают, что деление на ноль в PHP 8 бросает
DivisionByZeroError, а не выдаёт предупреждение. - Пишут
@перед вызовом, чтобы «спрятать» ошибку, вместо того чтобы обработать причину. - Забывают, что
finallyотработает даже приreturnвtry.
Как ответить кратко
«В PHP 7+ есть общий интерфейс
Throwable, от которого идут две независимые ветки:Exception— прикладные ошибки,Error— ошибки движка вродеTypeErrorилиDivisionByZeroError.catch (Exception)не поймаетError, для «поймать всё» нуженThrowable.LogicException— про ошибку программиста,RuntimeException— про внешний мир.finallyвыполняется всегда и служит для освобождения ресурсов, а на уровне приложения ставлюset_exception_handlerи логирую исключение целиком, а не одно сообщение».