Ошибки и исключения (try/catch)
Ошибки случаются. Главное - чтобы это происходило у тебя в логах, а не у пользователя в 3 ночи.
Ошибки vs исключения
В PHP есть два механизма обработки проблем:
Ошибки (errors) - генерирует сам PHP: Warning, Notice, Fatal Error. Их нельзя «поймать» обычным try/catch (до PHP 7).
Исключения (exceptions) - объекты, которые ты бросаешь (throw) и ловишь (catch). Это управляемый механизм.
<?php
// Ошибка - PHP сам сообщает:
echo $undefinedVariable; // Warning: Undefined variable
// Исключение - ты бросаешь сам:
throw new RuntimeException('Что-то не так');
Уровни ошибок PHP
<?php
// Notice - мелочь, но лучше исправить
echo $x; // Notice: Undefined variable
// Warning - серьёзнее, но скрипт продолжает работу
include 'файл-которого-нет.php'; // Warning
// Fatal Error - скрипт останавливается
вызов_несуществующей_функции(); // Fatal error
try/catch
<?php
try {
$result = riskyOperation();
echo "Успех: $result";
} catch (RuntimeException $e) {
echo 'Ошибка: ' . $e->getMessage();
}
Можно ловить несколько типов:
<?php
try {
$data = json_decode($input, true, 512, JSON_THROW_ON_ERROR);
$pdo->prepare('...')->execute($data);
} catch (\JsonException $e) {
echo 'Невалидный JSON: ' . $e->getMessage();
} catch (\PDOException $e) {
error_log('DB error: ' . $e->getMessage());
echo 'Ошибка базы данных';
}
В PHP 8+ можно ловить несколько типов в одном блоке:
<?php
try {
// ...
} catch (\JsonException | \InvalidArgumentException $e) {
echo 'Ошибка ввода: ' . $e->getMessage();
}
finally
Блок finally выполняется всегда - и при успехе, и при ошибке:
<?php
$file = fopen('data.txt', 'r');
try {
$content = fread($file, filesize('data.txt'));
// обработка...
} catch (\Throwable $e) {
error_log($e->getMessage());
} finally {
fclose($file); // закроется в любом случае
}
Иерархия исключений
Throwable (интерфейс)
├── Error (ошибки PHP - TypeError, DivisionByZeroError, etc.)
└── Exception
├── RuntimeException
├── InvalidArgumentException
├── LogicException
├── JsonException
└── PDOException
Свои исключения
<?php
declare(strict_types=1);
final class ValidationException extends \RuntimeException {
public function __construct(private readonly array $errors) {
parent::__construct('Ошибка валидации');
}
public function getErrors(): array {
return $this->errors;
}
}
function validateAge(string $input): int {
if (!ctype_digit($input)) {
throw new ValidationException(['age' => 'Должно быть числом']);
}
$age = (int)$input;
if ($age < 1 || $age > 150) {
throw new ValidationException(['age' => 'Возраст от 1 до 150']);
}
return $age;
}
Использование:
<?php
try {
$age = validateAge($_POST['age'] ?? '');
echo "Возраст: $age";
} catch (ValidationException $e) {
foreach ($e->getErrors() as $field => $message) {
echo "$field: $message\n";
}
}
set_exception_handler - глобальный обработчик
Если исключение не поймано нигде - оно попадёт сюда:
<?php
set_exception_handler(function (\Throwable $e) {
error_log("Unhandled: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}");
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => 'Internal Server Error']);
});
Это последняя линия обороны - ставь в точке входа (index.php), как в уроке про роутер.
Практический паттерн: обработка в API
<?php
declare(strict_types=1);
function handleRequest(): void {
try {
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// роутинг и обработка...
$result = processRoute($method, $path);
header('Content-Type: application/json');
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
} catch (ValidationException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage(), 'details' => $e->getErrors()]);
} catch (\JsonException $e) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
} catch (\PDOException $e) {
error_log('DB: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Database error']);
} catch (\Throwable $e) {
error_log('Fatal: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error']);
}
}
Типичные ошибки
catch (\Throwable $e)где попало. Ты ловишьTypeError,DivisionByZeroErrorи прочие баги PHP, маскируя их под бизнес-ошибки. Лови конкретные типы (ValidationException,\PDOException), а\Throwable- только в точке входа.- Пустой
catch- проглатывание исключений.catch (\Exception $e) {}молча теряет ошибку, ты узнаешь о баге через неделю от пользователей. Минимум -error_log($e); лучше - пробросить дальше черезthrow. - Возврат
false/nullвместоthrow. ФункцияfindUser()возвращаетfalseпри ошибке БД иnullпри «не найдено» - вызывающий код путается. Бросай исключение на нарушение инварианта, возвращайnullтолько на ожидаемое отсутствие. \RuntimeExceptionдля всего подряд. Без доменных типов нельзя селективно ловить -catch (\RuntimeException)поймает и валидацию, и сбой БД. ЗаводиValidationException,NotFoundException,PaymentDeclinedException.- Нет
set_exception_handlerв точке входа. Непойманное исключение вываливается стектрейсом в браузер с путями и SQL. Поставь обработчик вindex.php: лог +http_response_code(500)+ generic JSON.
Best practices
-
Бросай исключение при нарушении инварианта, возвращай значение при ожидаемом ветвлении (
nullдля «не найдено»,Resultдля «не удалось»). -
Лови самый конкретный тип, который реально умеешь обработать;
\Throwable- только на верхнем уровне. -
Заводи доменные исключения (
ValidationException extends \RuntimeException) - это часть контракта слоя. -
Никогда не пиши пустой
catch: либо логируй и пробрасывай, либо конвертируй в другой тип черезthrow new X(..., previous: $e). -
В Symfony не делай свой глобальный try/catch - подпишись на
kernel.exceptionчерезExceptionListenerили маппинг через#[AsEventListener], фреймворк сам конвертирует исключение в HTTP-ответ. -
Python - Исключения: try/except/else/finally и кастомные exceptions - тот же механизм в Python: иерархия BaseException, ExceptionGroup (3.11+)
Мини-задание
- Напиши функцию
parseAge(string $s): int- если не число или вне диапазона, бросайValidationException - Обработай
ValidationExceptionи\Throwableв разных catch-блоках - Создай свой
NotFoundException extends \RuntimeExceptionи используй его - Добавь
finallyдля закрытия ресурса (файл или соединение) - Настрой
set_exception_handlerкак глобальный обработчик