PHP try catch примеры обработки исключений с finally

Try catch PHP перехватывает исключения и запускает обработчик ошибки. Поместите рискованную операцию в try, а реакцию на сбой – в catch. Блок finally поможет освободить открытые ресурсы. Примеры показывают обработку разных типов, собственные исключения и запись подробностей в лог.

Пример Try Catch PHP

Начните с разбора JSON, который приложение получает от внешнего сервиса. Флаг JSON_THROW_ON_ERROR заставляет json_decode() выбросить JsonException при некорректном JSON.

Сохраните пример в файл example.php и выполните его установленным интерпретатором PHP командой php example.php.

<?php

$json = '{"city": }';

try {
    $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
    echo 'JSON прочитан: ' . $data['city'];
} catch (JsonException $e) {
    echo 'Ошибка JSON: ' . $e->getMessage();
}

echo PHP_EOL . 'Обработка завершена.';

Некорректная строка вызывает исключение, поэтому присваивание $data не завершается. PHP переходит в подходящий блок catch и выводит сообщение. После обработчика скрипт печатает «Обработка завершена».

Замените значение на '{"city": "Москва"}' и запустите файл повторно. Теперь программа выведет «JSON прочитан: Москва», пропустив обработчик ошибки.

Что делает каждый блок

  • try содержит операцию, которая может выбросить исключение. После выброса PHP пропускает оставшийся код этого блока.
  • catch перехватывает объект подходящего типа. Обработчик catch (JsonException $e) получает исключение в переменной $e.
  • finally выполняет завершающие действия при обычном выходе из конструкции, включая выход после исключения. В нем удобно закрывать файлы и освобождать другие ресурсы.

Обработчик не возвращает выполнение на строку, вызвавшую исключение. Если ошибка обработана без повторного выброса, выполнение продолжается после конструкции.

Синтаксис и особенности блоков описывает документация PHP об исключениях. Все примеры статьи подходят для PHP 8.x.

Catch без переменной и throw как выражение

Начиная с PHP 8.0, catch допускает отсутствие переменной исключения. Используйте такую запись, если подробности ошибки обработчику не нужны.

<?php

try {
    json_decode('{', true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
    echo 'Ответ сервиса содержит некорректный JSON.';
}

С PHP 8.0 оператор throw также работает внутри выражений. Например, оператор ?? позволяет выбросить исключение при отсутствии значения.

<?php

$input = [];

try {
    $name = $input['name']
        ?? throw new InvalidArgumentException('Не указано имя.');
    echo $name;
} catch (InvalidArgumentException $e) {
    echo $e->getMessage();
}

Какие ошибки перехватывают Exception и Throwable

В PHP 7+ интерфейс Throwable объединяет две основные ветви. Класс Exception представляет исключения, а класс Error – ошибки выполнения, представленные объектами.

  • Exception включает RuntimeException, InvalidArgumentException, JsonException и другие классы исключений.
  • Error включает TypeError, DivisionByZeroError и другие классы ошибок.
  • Throwable задает общий тип для обеих ветвей.

Блок catch (Exception $e) не перехватывает TypeError и DivisionByZeroError. Для перехвата обеих ветвей используйте catch (Throwable $e).

<?php

try {
    echo intdiv(10, 0);
} catch (Throwable $e) {
    echo 'Перехвачен тип: ' . get_class($e);
}

Программа выведет «Перехвачен тип: DivisionByZeroError» и завершится без непойманного исключения. Если заменить тип обработчика на Exception, ошибка останется неперехваченной.

Справка о Throwable описывает общий интерфейс и его методы. Широкий перехват удобен на границе приложения, где формируется ответ пользователю.

Обычные предупреждения PHP не превращаются в исключения автоматически. Например, неудачный fopen() может выдать Warning и вернуть false. Один только catch (Throwable $e) такое предупреждение не перехватит.

Как использовать несколько блоков catch

Разделяйте обработчики, если разные сбои требуют разной реакции приложения. PHP выбирает первый подходящий catch, остальные обработчики этой конструкции пропускает.

<?php

function decodeResponse(string $json): array
{
    $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);

    if (!is_array($data)) {
        throw new InvalidArgumentException('Ожидался объект или массив JSON.');
    }

    return $data;
}

try {
    $data = decodeResponse('{');
    echo 'Ответ принят.';
} catch (JsonException $e) {
    echo 'Ответ сервиса содержит ошибку JSON.';
} catch (InvalidArgumentException $e) {
    echo 'Структура ответа не подходит приложению.';
} catch (Exception $e) {
    echo 'Операция завершилась ошибкой.';
}

Ставьте частные типы перед общими, чтобы сохранить разные реакции. Если первым поставить Exception, он перехватит также JsonException и InvalidArgumentException.

Запустите пример с аргументами '{', 'null' и '{"ok": true}'. Первый вызовет ошибку JSON, второй – ошибку структуры. Третий завершится сообщением «Ответ принят».

Как объединить типы в одном catch

С PHP 7.1 синтаксис catch (A | B $e) объединяет несколько типов. Используйте его, когда обработка этих ошибок полностью совпадает.

В примере с функцией decodeResponse() замените цепочку обработчиков таким блоком:

catch (JsonException | InvalidArgumentException $e) {
    echo 'Сервис вернул неподходящий ответ.';
}

Оба типа попадут в один обработчик с общим сообщением. Объединенный catch не меняет класс самого объекта исключения.

Как закрыть ресурс в finally

Блок finally освобождает ресурс независимо от результата основной операции. Пример открывает временный поток, записывает JSON и читает его обратно.

<?php

$handle = null;
$json = '{"city": "Москва"}';

try {
    $handle = fopen('php://temp', 'w+');
    if ($handle === false) {
        throw new RuntimeException('Не удалось открыть временный поток.');
    }

    if (fwrite($handle, $json) === false) {
        throw new RuntimeException('Не удалось записать JSON.');
    }

    rewind($handle);
    $contents = stream_get_contents($handle);
    if ($contents === false) {
        throw new RuntimeException('Не удалось прочитать JSON.');
    }

    $data = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
    echo 'JSON прочитан.' . PHP_EOL;
} catch (JsonException | RuntimeException $e) {
    echo 'Обработка не выполнена.' . PHP_EOL;
} finally {
    if (is_resource($handle)) {
        fclose($handle);
        echo 'Ресурс закрыт.' . PHP_EOL;
    }
}

При корректном JSON скрипт напечатает «JSON прочитан» и «Ресурс закрыт». Замените $json на '{' и повторите запуск. Теперь появится «Обработка не выполнена», но ресурс все равно закроется.

Проверка is_resource() защищает вызов fclose() при неудачном открытии потока. Сам fopen() при сбое также может выдать предупреждение.

Блок finally выполняется и при return внутри try. Однако exit() прекращает скрипт без выполнения этого блока. Не используйте exit() внутри конструкции, если рассчитываете на освобождение ресурса.

Не возвращайте результат из finally: такой return может скрыть исключение. Оставьте этому блоку только завершающие действия.

Когда используются исключения?

Исключения используются, когда результат операции отличается от того, что ожидало ваше приложение. Типичные ситуации включают недоступную базу, ошибку запроса и некорректный ответ API.

Для чтения существующего CSV можно использовать SplFileObject. При невозможности открыть файл его конструктор выбрасывает RuntimeException.

<?php

try {
    $file = new SplFileObject(__DIR__ . '/my_file.csv', 'r');
    $row = $file->fgetcsv();
    echo 'CSV открыт.';
} catch (RuntimeException $e) {
    echo 'CSV-файл недоступен.';
}

Создайте рядом файл my_file.csv со строкой name,price. При запуске появится сообщение «CSV открыт». Переименуйте CSV и повторите запуск, чтобы получить «CSV-файл недоступен».

Как перехватить ошибку PDO

Режим PDO::ERRMODE_EXCEPTION заставляет PDO выбрасывать исключения при ошибках запросов. Ошибка подключения выбрасывает PDOException независимо от режима обработки ошибок.

Пример использует SQLite, чтобы не вводить учетные данные базы. Для запуска потребуется расширение pdo_sqlite.

<?php

try {
    $pdo = new PDO('sqlite::memory:', null, null, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]);

    $pdo->exec('CREATE TABLE items (id INTEGER PRIMARY KEY)');
    $pdo->query('SELECT * FROM missing_table');
} catch (PDOException $e) {
    echo 'Запрос к базе данных не выполнен.';
}

Таблицы missing_table нет, поэтому запрос попадет в catch. Замените ее имя на items, и запрос выполнится без сообщения об ошибке.

Для MySQL изменится строка подключения, но тип обработчика останется прежним.

Не выводите посетителю сообщение PDO: оно может раскрыть структуру базы.

Как создать и выбросить собственное исключение

Собственный класс позволяет отличить ошибку предметной области от технического сбоя. Наследуйте его от Exception или подходящего стандартного подкласса.

<?php

class MyException extends Exception
{
}

function reserveItems(int $requested, int $available): void
{
    if ($requested > $available) {
        throw new MyException('Недостаточно товара на складе.');
    }
}

try {
    reserveItems(3, 2);
    echo 'Товар зарезервирован.';
} catch (MyException $e) {
    echo $e->getMessage();
}

Вызов с числами 3 и 2 выведет сообщение о нехватке товара. Замените запрошенное количество на 1, чтобы увидеть успешное резервирование.

Конструкция throw new Exception('Сообщение') создает и сразу выбрасывает стандартное исключение. Собственный класс использует тот же синтаксис throw new, но задает отдельный тип для перехвата.

Нужно ли перехватывать все исключения?

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

Если погодный API недоступен, покажите «Информация о погоде недоступна». Остальная страница при этом может продолжить работу. Если приложение полностью зависит от базы, продолжать запрос после потери подключения обычно бессмысленно.

Не оставляйте пустые обработчики: они скрывают ошибки и затрудняют диагностику. Для непредвиденного сбоя сохраните подробности и передайте ошибку выше.

Повторный выброс и цепочка исключений

Оператор throw $e повторно выбрасывает тот же объект исключения. Используйте его после локальных действий, если окончательное решение принимает вызывающий код.

<?php

function decodeWithLog(string $json): mixed
{
    try {
        return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        error_log($e->getMessage());
        throw $e;
    }
}

try {
    decodeWithLog('{');
} catch (JsonException $e) {
    echo 'Не удалось прочитать ответ сервиса.';
}

Здесь локальный обработчик записывает сообщение в журнал PHP. Внешний обработчик получает то же исключение и формирует ответ.

Для добавления контекста передайте исходное исключение третьим аргументом конструктора. Метод getPrevious() затем вернет исходную причину.

<?php

function parseConfig(string $json): mixed
{
    try {
        return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        throw new RuntimeException('Ошибка чтения конфигурации.', 0, $e);
    }
}

try {
    parseConfig('{');
} catch (RuntimeException $e) {
    echo $e->getMessage();
    $previous = $e->getPrevious();
    if ($previous !== null) {
        error_log($previous->getMessage());
    }
}

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

Глобальный обработчик и запись подробностей в лог

Функция set_exception_handler() назначает обработчик непойманных исключений. Зарегистрируйте его до выполнения основного кода приложения.

Этот самостоятельный пример предназначен для запуска командой php example.php. Он записывает журнал в файл рядом с примером. Каталог должен разрешать запись пользователю, который запускает PHP.

<?php

set_exception_handler(function (Throwable $e): void {
    $details = sprintf(
        "[%s] %s; code=%s; file=%s; line=%d\n%s\n",
        get_class($e),
        $e->getMessage(),
        (string) $e->getCode(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    );

    error_log($details, 3, __DIR__ . '/app-error.log');

    if (PHP_SAPI !== 'cli') {
        http_response_code(500);
    }
    echo 'Операция недоступна. Попробуйте позднее.';
});

throw new RuntimeException('Тестовая ошибка приложения.', 1001);

echo 'Эта строка не выполнится.';

На экране появится только заглушка «Операция недоступна. Попробуйте позднее». Файл app-error.log получит сообщение, код 1001, имя файла, строку и стек вызовов.

  • getMessage() возвращает текст ошибки.
  • getCode() возвращает код, заданный при создании исключения.
  • getFile() и getLine() указывают место возникновения исключения.
  • getTraceAsString() возвращает стек вызовов текстовой строкой.

Глобальный обработчик не возобновляет выполнение после проблемной строки. После его завершения скрипт останавливается, поэтому последнее echo не сработает.

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

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

Выбирайте обработчик по тому, какую реакцию способен выполнить ваш код.

  • Если операция допускает запасной результат, обработайте конкретный тип рядом с вызовом.
  • Если открыли ресурс, освободите его в finally.
  • Если нужно добавить контекст, создайте исключение с исходной причиной.
  • Если локальный код не может восстановить работу, передайте исключение выше.
  • Если исключение осталось непойманным, глобальный обработчик запишет подробности и покажет заглушку.

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

Комментарии

Оставьте свой комментарий
А
Андрей

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

БГ
Борис Г.

Здравствуйте! Не до конца понял, зачем использовать try и catch. Ведь можно логикой if else оценить результаты ошибочной операции и вывести пользователю понятные сообщения о том, что именно пошло не так

Д
Дмитрий

В том-то и дело, зачем городить конструкцию try-catch, если if-else можно буквально одной строкой сделать то же самое?
Очевидно, что try-catch должен использоваться в каких-то более сложных моментах.