Правильная обработка ошибок

Существует две фундаментальные стратегии: обработка исправимых ошибок (исключения, коды возврата по ошибке, функции-обработчики) и неисправимых (assert(), abort()). В каких случаях какую стратегию лучше использовать?

Виды ошибок

Ошибки возникают по разным причинам: пользователь ввёл странные данные, ОС не может дать вам обработчика файла или код разыменовывает (dereferences) nullptr. Каждая из описанных ошибок требует к себе отдельного подхода. По причинам ошибки делятся на три основные категории:

  • Пользовательские ошибки: здесь под пользователем подразумевается человек, сидящий перед компьютером и действительно «использующий» программу, а не какой-то программист, дёргающий ваш API. Такие ошибки возникают тогда, когда пользователь делает что-то неправильно.
  • Системные ошибки появляются, когда ОС не может выполнить ваш запрос. Иными словами, причина системных ошибок — сбой вызова системного API. Некоторые возникают потому, что программист передал системному вызову плохие параметры, так что это скорее программистская ошибка, а не системная.
  • Программистские ошибки случаются, когда программист не учитывает предварительные условия API или языка программирования. Если API требует, чтобы вы не вызывали foo() с 0 в качестве первого параметра, а вы это сделали, — виноват программист. Если пользователь ввёл 0, который был передан foo(), а программист не написал проверку вводимых данных, то это опять же его вина.

Каждая из описанных категорий ошибок требует особого подхода к их обработке.

Пользовательские ошибки

Сделаю очень громкое заявление: такие ошибки — на самом деле не ошибки.

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

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

Конечно, такое не всегда возможно. Иногда проверять вводимые данные слишком дорого, иногда это не позволяет сделать архитектура кода или разделение ответственности. Но в таких случаях ошибки должны обрабатываться однозначно как исправимые. Иначе, допустим, ваша офисная программа будет падать из-за того, что вы нажали backspace в пустом документе, или ваша игра станет вылетать при попытке выстрелить из разряженного оружия.

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

Системные ошибки

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

Но как их обрабатывать, как исправимые или неисправимые?

Это зависит от обстоятельств.

Многие считают, что ошибка нехватки памяти — неисправимая. Зачастую не хватает памяти даже для обработки этой ошибки! И тогда приходится просто сразу же прерывать выполнение.

Но падение программы из-за того, что ОС не может выделить сокет, — это не слишком дружелюбное поведение. Так что лучше бросить исключение и позволить catch аккуратно закрыть программу.

Но бросание исключения — не всегда правильный выбор.

Кто-то даже скажет, что он всегда неправильный.

Если вы хотите повторить операцию после её сбоя, то обёртывание функции в try-catch в цикле — медленное решение. Правильный выбор — возврат кода ошибки и цикличное исполнение, пока не будет возвращено правильное значение.

Если вы создаёте вызов API только для себя, то просто выберите подходящий для своей ситуации путь и следуйте ему. Но если вы пишете библиотеку, то не знаете, чего хотят пользователи. Дальше мы разберём подходящую стратегию для этого случая. Для потенциально неисправимых ошибок подойдёт «обработчик ошибок», а при других ошибках необходимо предоставить два варианта развития событий.

Обратите внимание, что не следует использовать подтверждения (assertions), включающиеся только в режиме отладки. Ведь системные ошибки могут возникать и в релизной сборке!

Программистские ошибки

Это худший вид ошибок. Для их обработки я стараюсь сделать так, чтобы мои ошибки были связаны только с вызовами функций, то есть с плохими параметрами. Прочие типы программистских ошибок могут быть пойманы только в runtime, с помощью отладочных макросов (assertion macros), раскиданных по коду.

При работе с плохими параметрами есть две стратегии: дать им определённое или неопределённое поведение.

Если исходное требование для функции — запрет на передачу ей плохих параметров, то, если их передать, это считается неопределённым поведением и должно проверяться не самой функцией, а оператором вызова (caller). Функция должна делать только отладочное подтверждение (debug assertion).

С другой стороны, если отсутствие плохих параметров не является частью исходных требований, а документация определяет, что функция будет бросать bad_parameter_exception при передаче ей плохого параметра, то передача — это хорошо определённое поведение (бросание исключения или любая другая стратегия обработки исправимых ошибок), и функция всегда должна это проверять.

В качестве примера рассмотрим получающие функции (accessor functions) std::vector<T>: в спецификации на operator[] говорится, что индекс должен быть в пределах валидного диапазона, при этом at() сообщает нам, что функция кинет исключение, если индекс не попадает в диапазон. Более того, большинство реализаций стандартных библиотек обеспечивают режим отладки, в котором проверяется индекс operator[], но технически это неопределённое поведение, оно не обязано проверяться.

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

Когда нужно проверять только с помощью отладочных подтверждений, а когда — постоянно?

К сожалению, однозначного рецепта нет, решение зависит от конкретной ситуации. У меня есть лишь одно проверенное правило, которому я следую при разработке API. Оно основано на наблюдении, что проверять исходные условия должен вызывающий, а не вызываемый. А значит, условие должно быть «проверяемым» для вызывающего. Также условие «проверяемое», если можно легко выполнить операцию, при которой значение параметра всегда будет правильным. Если для параметра это возможно, то это получается исходное условие, а значит, проверяется только посредством отладочного подтверждения (а если слишком дорого, то вообще не проверяется).

Но конечное решение зависит от многих других факторов, так что очень трудно дать какой-то общий совет. По умолчанию я стараюсь свести к неопределённому поведению и использованию только подтверждений. Иногда бывает целесообразно обеспечить оба варианта, как это делает стандартная библиотека с operator[] и at().

Хотя в ряде случаев это может быть ошибкой.

Об иерархии std::exception

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

Я предлагаю наследовать только от одного из этих четырёх классов:

  • std::bad_alloc: для сбоев выделения памяти.
  • std::runtime_error: для общих runtime-ошибок.
  • std::system_error (производное от std::runtime_error): для системных ошибок с кодами ошибок.
  • std::logic_error: для программистских ошибок с определённым поведением.

Обратите внимание, что в стандартной библиотеке разделяются логические (то есть программистские) и runtime-ошибки. Runtime-ошибки — более широкое определение, чем «системные». Оно описывает «ошибки, обнаруживаемые только при выполнении программы». Такая формулировка не слишком информативна. Лично я использую её для плохих параметров, которые не являются исключительно программистскими ошибками, а могут возникнуть и по вине пользователей. Но это можно определить лишь глубоко в стеке вызовов. Например, плохое форматирование комментариев в standardese приводит к исключению при парсинге, проистекающему из std::runtime_error. Позднее оно ловится на соответствующем уровне и фиксируется в логе. Но я не стал бы использовать этот класс иначе, как и std::logic_error.

Подведём итоги

Есть два пути обработки ошибок:

  • как исправимые: используются исключения или возвращаемые значения (в зависимости от ситуации/религии);
  • как неисправимые: ошибки журналируются, а программа прерывается.

Подтверждения — это особый вид стратегии обработки неисправимых ошибок, только в режиме отладки.

Есть три основных источника ошибок, каждый требует особого подхода:

  • Пользовательские ошибки не должны обрабатываться как ошибки на верхних уровнях программы. Всё, что вводит пользователь, должно проверяться соответствующим образом. Это может обрабатываться как ошибки только на нижних уровнях, которые не взаимодействуют с пользователями напрямую. Применяется стратегия обработки исправимых ошибок.
  • Системные ошибки могут обрабатываться в рамках любой из двух стратегий, в зависимости от типа и тяжести. Библиотеки должны работать как можно гибче.
  • Программистские ошибки, то есть плохие параметры, могут быть запрещены исходными условиями. В этом случае функция должна использовать только проверку с помощью отладочных подтверждений. Если же речь идёт о полностью определённом поведении, то функции следует предписанным образом сообщать об ошибке. Я стараюсь по умолчанию следовать сценарию с неопределённым поведением и определяю для функции проверку параметров лишь тогда, когда это слишком трудно сделать на стороне вызывающего.

Гибкие методики обработки ошибок в C++

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

Это относительно легко сделать в высокоуровневых функциях. Вы точно знаете, почему что-то пошло не так, и можете обработать это соответствующим образом. Но в случае с низкоуровневыми функциями всё не так просто. Они не знают, что пошло не так, они знают лишь о самом факте сбоя и должны сообщить об этом тому, кто их вызвал.

В C++ есть два основных подхода: коды возврата ошибок и исключения. Сегодня широко распространено использование исключений. Но некоторые не могут / думают, что не могут / не хотят их использовать — по разным причинам.

Я не буду принимать чью-либо сторону. Вместо этого я опишу методики, которые удовлетворят сторонников обоих подходов. Особенно методики пригодятся разработчикам библиотек.

Проблема

Я работаю над проектом foonathan/memory. Это решение предоставляет различные классы выделения памяти (allocator classes), так что в качестве примера рассмотрим структуру функции выделения.

Для простоты возьмём malloc(). Она возвращает указатель на выделяемую память. Если выделить память не получается, то возвращается nullptr, то есть NULL, то есть ошибочное значение.

У этого решения есть недостатки: вам нужно проверять каждый вызов malloc(). Если вы забудете это сделать, то выделите несуществующую память. Кроме того, по своей натуре коды ошибок транзитивны: если вызвать функцию, которая может вернуть код ошибки, и вы не можете его проигнорировать или обработать, то вы тоже должны вернуть код ошибки.

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

Это можно расценить как недостаток.

Но в подобных ситуациях исключения имеют также очень большое преимущество: функция выделения памяти либо возвращает валидную память, либо вообще ничего не возвращает. Это функция «всё или ничего», возвращаемое значение всегда будет валидным. Это полезное следствие согласно принципу Скотта Майера «Make interfaces hard to use incorrectly and easy to use correctly».

Учитывая вышесказанное, можно утверждать, что вам следует использовать исключения в качестве механизма обработки ошибок. Этого мнения придерживается большинство разработчиков на С++, включая и меня. Но проект, которым я занимаюсь, — это библиотека, предоставляющая средства выделения памяти, и предназначена она для приложений, работающих в реальном времени. Для большинства разработчиков подобных приложений (особенно для игроделов) само использование исключений — исключение.

Каламбур детектед.

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

Так что же делать?

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

К счастью, я могу определить, что вы делаете, когда обнаруживаете ошибку нехватки памяти: чаще всего вы журналируете это событие и прерываете программу, поскольку она не может корректно работать без памяти. В таких ситуациях исключения — просто способ передачи контроля другой части кода, которая журналирует и прерывает программу. Но есть старый и эффективный способ передачи контроля: указатель функции (function pointer), то есть функция-обработчик (handler function).

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

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

Решение 1: обработчик исключений

Если вам нужно обработать ошибку в условиях, когда наиболее распространённым поведением будет «журналировать и прервать», то можно использовать обработчика исключений. Это такая функция-обработчик, которая вызывается вместо бросания объекта-исключения. Её довольно легко реализовать даже в уже существующем коде. Для этого нужно поместить управление обработкой в класс исключений и обернуть в макрос выражение throw.

Сначала дополним класс и добавим функции для настройки и, возможно, запрашивания функции-обработчика. Я предлагаю делать это так же, как стандартная библиотека обрабатывает std::new_handler:

class my_fatal_error
{
public:
    // тип обработчика, он должен брать те же параметры, что и конструктор,
    // чтобы у них была одинаковая информация
    using handler = void(*)( ... );

    // меняет функцию-обработчика
    handler set_handler(handler h);

    // возвращает текущего обработчика
    handler get_handler();

    ... // нормальное исключение
};

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

Если исключения включены, то для удаления обработчика можно использовать условное компилирование (conditional compilation). Если хотите, то также напишите обычный подмешанный класс (mixin class), дающий требуемую функциональность.

Конструктор исключений элегантен: он вызывает текущую функцию-обработчика, передавая ей требуемые аргументы из своих параметров. А затем комбинирует с последующим макросом throw:

If```cpp #if EXCEPTIONS #define THROW(Ex) throw (Ex) #else #define THROW(Ex) (Ex), std::abort() #endif

> Такой макрос throw также предоставляется [foonathan/compatiblity](https://github.com/foonathan/compatibility).

Можно использовать его и так:

```cpp
THROW(my_fatal_error(...))

Если у вас включена поддержка исключений, то будет создан и брошен объект-исключение, всё как обычно. Но если поддержка выключена, то объект-исключение всё равно будет создан, и — это важно — только после этого произойдёт вызов std::abort(). А поскольку конструктор вызывает функцию-обработчика, то он и работает, как требуется: вы получаете точку настройки для журналирования ошибки. Благодаря же вызову std::abort() после конструктора пользователь не может нарушить постусловие.

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

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

А если я хочу продолжить работу после бросания исключения?

Методика с обработчиком исключений не позволяет этого сделать в связи с постусловием кода. Как же тогда продолжить работу?

Ответ прост — никак. По крайней мере, это нельзя сделать так же просто, как в других случаях. Нельзя просто так вернуть код ошибки вместо исключения, если функция на это не рассчитана.
Есть только одно решение: сделать две функции. Одна возвращает код ошибки, а вторая бросает исключения. Клиенты, которым нужны исключения, будут использовать второй вариант, остальные — первый.

Извините, что говорю такие очевидные вещи, но ради полноты изложения я должен был об этом сказать.

Для примера снова возьмём функцию выделения памяти. В этом случае я использую такие функции:

void* try_malloc(..., int &error_code) noexcept;

void* malloc(...);

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

void* malloc(...)
{
    auto error_code = 0;
    auto res = try_malloc(..., error_code);
    if (!res)
        throw malloc_error(error_code);
    return res;
}

Не делайте этого в обратной последовательности, иначе вам придётся ловить исключение, а это дорого. Также это не даст нам скомпилировать код без включённой поддержки исключений. Если сделаете, как показано, то можете просто стереть другую перегрузку (overload) с помощью условного компилирования.

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

Решение 2: предоставить две перегрузки

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

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

Пожалуйста, не используйте глобальную переменную errno или что-то типа GetLastError()!

Если возвращаемое значение не содержит недопустимое значение для обозначения сбоя, то по мере возможности используйте std::optional или что-то похожее.

Перегрузка исключения (exception overload) может — и должна — быть реализована в рамках версии с кодом ошибки, как это показано выше. Если компилируете без исключений, сотрите перегрузку с помощью условного компилирования.

std::system_error

Подобная система идеально подходит для работы с кодами ошибок в С++ 11.

Она возвращает непортируемый (non-portable) код ошибки std::error_code, то есть возвращаемый функцией операционной системы. С помощью сложной системы библиотечных средств и категорий ошибок вы можете добавить собственные коды ошибок, или портируемые std::error_condition. Для начала почитайте об этом здесь. Если нужно, то можете использовать в функции кода ошибки std::error_code. А для функции исключения есть подходящий класс исключения: std::system_error. Он берёт std::error_code и применяется для передачи этих ошибок в виде исключений.

Эту или подобную систему должны использовать все низкоуровневые функции, являющиеся закрытыми обёртками ОС-функций. Это хорошая — хотя и сложная — альтернатива службе кодов ошибок, предоставляемой операционной системой.

Да, и мне ещё нужно добавить подобное в функции виртуальной памяти. На сегодняшний день они не предоставляют коды ошибок.

std::expected

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

А глобальные переменные вообще не вариант!

В № 4109 предложено решение: std::expected. Это шаблон класса, который также хранит возвращаемое значение или код ошибки. В вышеприведённом примере он мог бы использоваться так:

std::expected<void*, std::error_code> try_malloc(...);

В случае успеха std::expected будет хранить не-null указатель памяти, а при сбое — std::error_code. Сейчас эта методика работает при любых возвращаемых значениях. Комбинация std::expected и функции исключения определённо допускает любые варианты использования.

Заключение

Если вы создаёте библиотеки, то иногда приходится обеспечивать максимальную гибкость использования. Под этим подразумевается и разнообразие средств обработки ошибок: иногда требуются коды возврата, иногда — исключения.

Одна из возможных стратегий — улаживание этих противоречий с помощью обработчика исключений. Просто удостоверьтесь, что когда нужно, то вызывается callback, а не бросается исключение. Это замена для критических ошибок, которая в любом случае будет журналироваться перед прерыванием работы программы. Как таковой этот способ не универсален, вы не можете переключаться в одной программе между двумя версиями. Это лишь обходное решение при отключённой поддержке исключений.

Более гибкий подход — просто предоставить две перегрузки, одну с исключениями, а вторую без. Это даст пользователям максимальную свободу, они смогут выбирать ту версию, что лучше подходит в их ситуации. Недостаток этого подхода: вам придётся больше потрудиться при создании библиотеки.

Хотя PHP уже давно поддерживает обработку исключений, однако, по сравнению с Java эта поддержка была довольно слабой

Первоначальная поддержка обработки исключений была введена в язык с 5 версии PHP, с двумя простыми встроенными классами исключений — Exception и ErrorException, с поддержкой дополнительных классов через SPL. Идея этого поста состоит в том, чтобы представить читателям современные возможности обработки исключений PHP. 

Новый интерфейс

Хотя PHP 7 предоставляет классы Error и Exception, давайте сначала затронем интерфейс Throwable . И Error и Exception классы реализуют Throwable интерфейс — это основа для любого объекта , который может быть брошен с помощью оператора throw. Единственное, что он не может быть реализован непосредственно в классах пользовательского пространства, только через расширение класса Exception. Кроме того, он обеспечивает единую точку для отлова обоих типов ошибок в одном выражении:

<?php

try {
// ваш код
} catch (Throwable $e) {
echo 'Очень хороший способ отловить исключения и ошибки';
}

Список доступных встроенных классов исключений начиная с PHP 7.4:

  • Exception
  • ErrorException
  • Error
  • ArgumentCountError
  • ArithmeticError
  • AssertionError
  • DivisionByZeroError
  • CompileError
  • ParseError
  • TypeError

Дополнительные классы исключений можно найти в стандартной библиотеке PHP . И наиболее заметным из расширений JSON является класс JsonException.

THROWABLE

Интерфейс Throwable  PHP 7:

interface Throwable
{
public function getMessage(): string; // Error reason
public function getCode(): int; // Error code
public function getFile(): string; // Error begin file
public function getLine(): int; // Error begin line
public function getTrace(): array; // Return stack trace as array like debug_backtrace()
public function getTraceAsString(): string; // Return stack trace as string
public function getPrevious(): Throwable; // Return previous `Trowable`
public function __toString(): string; // Convert into string
}

Вот иерархия Throwable:

interface Throwable
|- Error implements Throwable
|- ArithmeticError extends Error
|- DivisionByZeroError extends ArithmeticError
|- AssertionError extends Error
|- ParseError extends Error
|- TypeError extends Error
|- ArgumentCountError extends TypeError
|- Exception implements Throwable
|- ClosedGeneratorException extends Exception
|- DOMException extends Exception
|- ErrorException extends Exception
|- IntlException extends Exception
|- LogicException extends Exception
|- BadFunctionCallException extends LogicException
|- BadMethodCallException extends BadFunctionCallException
|- DomainException extends LogicException
|- InvalidArgumentException extends LogicException
|- LengthException extends LogicException
|- OutOfRangeException extends LogicException
|- PharException extends Exception
|- ReflectionException extends Exception
|- RuntimeException extends Exception
|- OutOfBoundsException extends RuntimeException
|- OverflowException extends RuntimeException
|- PDOException extends RuntimeException
|- RangeException extends RuntimeException
|- UnderflowException extends RuntimeException
|- UnexpectedValueException extends RuntimeException

Ошибка, почему?

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

Проблема заключалась в том, что было несколько фатальных ошибок, которые не могли быть обработаны определяемым пользователем обработчиком ошибок. Это означало, что вы не могли корректно обрабатывать фатальные ошибки в PHP. Было несколько побочных эффектов, которые были проблематичными, такие как потеря контекста времени выполнения, деструкторы не вызывались, да и вообще иметь дело с ними было неудобно. В PHP 7 фатальные ошибки теперь являются исключениями, и мы можем легко их обработать. Фатальные ошибки приводят к возникновению исключений. Вам необходимо обрабатывать нефатальные ошибки с помощью функции обработки ошибок.

Вот пример ловли фатальной ошибки в PHP 7.1. Обратите внимание, что нефатальная ошибка не обнаружена.

<?php 

try {
// будет генерировать уведомление, которое не будет поймано
echo $someNotSetVariable;
// фатальная ошибка, которая сейчас на самом деле ловится
someNoneExistentFunction();
} catch (Error $e) {
echo "Error caught: " . $e->getMessage();
}

Этот скрипт выведет сообщение об ошибке при попытке доступа к недопустимой переменной. Попытка вызвать функцию, которая не существует, приведет к фатальной ошибке в более ранних версиях PHP, но в PHP 7.1 вы можете ее перехватить. Вот вывод для скрипта:

Notice: Undefined variable: someNotSetVariable on line 3
Error caught: Call to undefined function someNoneExistentFunction()

Константы ошибок

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

Вот некоторые из наиболее часто встречающихся кодов ошибок:

  • E_DEPRECATED — интерпретатор сгенерирует этот тип предупреждений, если вы используете устаревшую языковую функцию. Сценарий обязательно продолжит работать без ошибок.
  • E_STRICT — аналогично E_DEPRECATED, — указывает на то, что вы используете языковую функцию, которая не является стандартной в настоящее время и может не работать в будущем. Сценарий будет продолжать работать без каких-либо ошибок.
  • E_PARSE — ваш синтаксис не может быть проанализирован, поэтому ваш скрипт не запустится. Выполнение скрипта даже не запустится.
  • E_NOTICE — движок просто выведет информационное сообщение. Выполнение скрипта не прервется, и ни одна из ошибок не будет выдана.
  • E_ERROR — скрипт не может продолжить работу, и завершится. Выдает ошибки, а как они будут обрабатываться, зависит от обработчика ошибок.
  • E_RECOVERABLE_ERROR — указывает на то, что, возможно, произошла опасная ошибка, и движок работает в нестабильном состоянии. Дальнейшее выполнение зависит от обработчика ошибок, и ошибка обязательно будет выдана.

Полный список констант можно найти в руководстве по PHP.

Функция обработчика ошибок

Функция set_error_handler() используется, чтобы сообщить PHP как обрабатывать стандартные ошибки, которые не являются экземплярами класса исключений Error. Вы не можете использовать функцию обработчика ошибок для фатальных ошибок. Исключения ошибок должны обрабатываться с помощью операторов try/catch. set_error_handler() принимает callback функцию в качестве своего параметра. Callback-функции в PHP могут быть заданы двумя способами: либо строкой, обозначающей имя функции, либо передачей массива, который содержит объект и имя метода (именно в этом порядке). Вы можете указать защищенные и приватные методы для callable в объекте. Вы также можете передать значение null, чтобы указать PHP вернуться к использованию стандартного механизма обработки ошибок. Если ваш обработчик ошибок не завершает программу и возвращает результат, ваш сценарий будет продолжать выполняться со строки, следующей за той, где произошла ошибка.

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

Вот пример:

<?php

function myCustomErrorHandler(int $errNo, string $errMsg, string $file, int $line) {
echo "Ух ты, мой обработчик ошибок получил #[$errNo] в [$file] на [$line]: [$errMsg]";
}

set_error_handler('myCustomErrorHandler');

try {
why;
} catch (Throwable $e) {
echo 'И моя ошибка: ' . $e->getMessage();
}

Если вы запустите этот код в PHP-консоли php -a, вы должны получить похожий вывод:

Error #[2] occurred in [php shell code] at line [3]: [Use of undefined constant why - assumed 'why' (this will throw an Error in a future version of PHP)]

Самые известные PHP библиотеки , которые делают обширное использование РНР set_error_handler() и могут сделать хорошие представления исключений и ошибок являются whoops,  и Symony Debug, ErrorHandler компоненты. Я смело рекомендую использовать один из них. Если не собираетесь их использовать в своем проекте, то вы всегда можете черпать вдохновение из их кода. В то время как компонент Debug широко используется в экосистеме Symfony, Whoops остается библиотекой выбора для фреймворка Laravel . 

Для подробного и расширенного использования, пожалуйста, обратитесь к руководству по PHP для обработчика ошибок.

Отображение или подавление нефатальной ошибки

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

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

Для этого вам нужно настроить PHP, используя следующие параметры в вашем файле php.ini:

  • display_errors – может быть установлен в false для подавления сообщений
  • log_errors – может использоваться для хранения сообщений об ошибках в файлах журнала
  • error_reporting – можно настроить, какие ошибки вызывают отчет

Лучше всего корректно обрабатывать ошибки в вашем приложении. В производственном процессе вы должны скорее регистрировать необработанные ошибки, чем разрешать их отображение пользователю. Функция error_log() может использоваться для отправки сообщения одной из определенных процедур обработки ошибок. Вы также можете использовать функцию error_log() для отправки электронных писем, но лично вы бы предпочли использовать хорошее решение для регистрации ошибок и получения уведомлений при возникновении ошибок, например Sentry или Rollbar .

Существует вещь, называемая оператором контроля ошибок ( @ ), который по своей сути может игнорировать и подавлять ошибки. Использование очень простое — просто добавьте любое выражение PHP с символом «собаки», и сгенерированная ошибка будет проигнорирована. Хотя использование этого оператора может показаться интересным, я призываю вас не делать этого. Мне нравится называть это живым пережитком прошлого.

Больше информации для всех функций, связанных с ошибками PHP, можно найти в руководстве.

Исключения (Exceptions)

Исключения являются основной частью объектно-ориентированного программирования и впервые были представлены в PHP 5.0. Исключением является состояние программы, которое требует специальной обработки, поскольку оно не выполняется ожидаемым образом. Вы можете использовать исключение, чтобы изменить поток вашей программы, например, чтобы прекратить что-либо делать, если некоторые предварительные условия не выполняются.

Исключение будет возникать в стеке вызовов, если вы его не перехватите. Давайте посмотрим на простой пример:

try {
print "это наш блок попыток n";
throw new Exception();
} catch (Exception $e) {
print "что-то пошло не так, есть улов!";
} finally {
print "эта часть всегда выполняется";
}

PHP включает в себя несколько стандартных типов исключений, а стандартная библиотека PHP (SPL) включает в себя еще несколько. Хотя вам не нужно использовать эти исключения, это означает, что вы можете использовать более детальное обнаружение ошибок и отчеты. Классы Exception и Error реализуют интерфейс Throwable и, как и любые другие классы, могут быть расширены. Это позволяет вам создавать гибкие иерархии ошибок и адаптировать обработку исключений. Только класс, который реализует класс Throwable, может использоваться с ключевым словом throw. Другими словами, вы не можете объявить свой собственный базовый класс и затем выбросить его как исключение.

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

Ловля исключений

Вы должны использовать try/catch структуру:

<?php

class MyCustomException extends Exception { }

function throwMyCustomException() {
throw new MyCustomException('Здесь что-то не так.');
}

try {
throwMyCustomException();
} catch (MyCustomException $e) {
echo "Ваше пользовательское исключение поймано";
echo $e->getMessage();
} catch (Exception $e) {
echo "Стандартное исключение PHP";
}

Как видите, есть два предложения catch. Исключения будут сопоставляться с предложениями сверху вниз, пока тип исключения не будет соответствовать предложению catch. Эта очень простая функция throwMyCustomException() генерирует исключение MyCustomException, и мы ожидаем, что оно будет перехвачено в первом блоке. Любые другие исключения, которые произойдут, будут перехвачены вторым блоком. Здесь мы вызываем метод getMessage() из базового класса Exception. Вы можете найти больше информации о дополнительном методе в Exception PHP docs.

Кроме того, можно указать несколько исключений, разделяя их трубой ( | ).

Давайте посмотрим на другой пример:

<?php

class MyCustomException extends Exception { }
class MyAnotherCustomException extends Exception { }

try {
throw new MyAnotherCustomException;
} catch (MyCustomException | MyAnotherCustomException $e) {
echo "Caught : " . get_class($e);
}

Этот очень простой блок catch будет перехватывать исключения типа MyCustomException и MyAnotherCustomException.

Немного более продвинутый сценарий:

// exceptions.php
use SymfonyComponentHttpKernelExceptionNotFoundHttpException;

try {
throw new NotFoundHttpException();
} catch (Exception $e) {
echo 1;
} catch (NotFoundHttpException $e) {
echo 2;
} catch (Exception $e) {
echo 3;
} finally {
echo 4;
}

Это ваш окончательный ответ?

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

<?php

class MyCustomException extends Exception { }

function throwMyCustomException() {
throw new MyCustomException('Здесь что-то не так');
}

try {
throwMyCustomException();
} catch (MyCustomException $e) {
echo "Ваше пользовательское исключение поймано ";
echo $e->getMessage();
} catch (Exception $e) {
echo "Стандартное исключение PHP";
} finally {
echo "Я всегда тут";
}

Вот хороший пример того, как работают операторы PHP catch/finally:

<?php

try {
try {
echo 'a-';
throw new exception();
echo 'b-';
} catch (Exception $e) {
echo 'пойманный-';
throw $e;
} finally {
echo 'завершенный-';
}
} catch (Exception $e) {
echo 'конец-';
}

Функция обработчика исключений

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

Для этого вы используете функцию set_exception_handler() , которая принимает вызываемый элемент в качестве параметра. Ваш сценарий завершится после того, как вызов будет выполнен.

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

<?php

class MyCustomException extends Exception { }

function exception_handler($exception) {
echo "Uncaught exception: " , $exception->getMessage(), "n";
}

set_exception_handler('exception_handler');

try {
throw new Exception('Uncaught Exception');
} catch (MyCustomException $e) {
echo "Ваше пользовательское исключение поймано ";
echo $e->getMessage();
} finally {
echo "Я всегда тут";
}

print "Не выполнено";

Здесь простая функция exception_handler будет выполняться после блока finally, когда ни один из типов исключений не был сопоставлен. Последний вывод никогда не будет выполнен.

Для получения дополнительной информации обратитесь к документации PHP. 

Старый добрый «T_PAAMAYIM_NEKUDOTAYIM»

Вероятно, это было самое известное сообщение об ошибке PHP. В последние годы было много споров по этому поводу. Вы можете прочитать больше в отличном сообщении в блоге от Фила Осетра .  

Сегодня я с гордостью могу сказать, что если вы запустите этот код с PHP 7, то сообщение о T_PAAMAYIM_NEKUDOTAYIM больше не будет:

<?php

class foo
{
static $bar = 'baz';
}

var_dump('foo'::$bar);

// Output PHP < 7.0:
// PHP Parse error: syntax error, unexpected '::' (T_PAAMAYIM_NEKUDOTAYIM) in php shell code on line 1
// Output PHP > 7.0:
// string(3) "baz"
?>

В заключении

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

Что я понимаю под правильной обработкой:

  • Универсальное решение, которое можно вставить в любой существующий код;
  • Легко расширяемое решение;
  • В PHP аж три «механизма ошибок»: собственно ошибки (error), исключения (exception) и утверждения (assertion). Свести три механизма к одному — exception. В комментариях к предыдущей статье на эту тему выражалось мнение, что exception это плохой и/или сложный метод обработки ошибок. Я так не считаю и готов это обсудить в комментариях;
  • Опциональное логирование;
  • Общий обработчик exception, который будет поддерживать разные форматы вывода и debug/production режимы;
  • В debug режиме должен выводится trace. Требования к trace: компактный, понятный и по возможности ссылки на открытие файлов в IDE.

Универсальное решение

Для этого сделаем класс exceptionHandlerClass. В exceptionHandlerClass будут храниться настройки и статические методы — обработчики error, exception и assertion. Еще нам нужны методы setupHandlers и restoreHandlers. Первый метод настроит перехват ошибок. Error и assertion обработчики будут бросать ErrorException. Exception обработчик будет обрабатывать необработанные Exception и в зависимости от настроек выводить соответствующий ответ. restoreHandlers вернет все обработчики в изначальное состояние — это поможет при встраивании класса в код с существующим механизмом обработки ошибок. Подключение выглядит так:

  1. require ‘exceptionHandler/exceptionHandlerClass.php’;
  2. exceptionHandlerClass::setupHandlers();

включение debug режима (по умолчанию выключен) :

  1. exceptionHandlerClass::$debug = true;

Форматы вывода

Проще объяснить на примере: для вывода trace на веб странице я оберну его в таги pre и применю htmlspecialchars(), с другой стороны этот же trace при выводе в консоль будет не удобно читать, было бы проще, если бы это был plainText. Если нужно вывести ошибку как ответ SоapServer, то это должен быть правильно сформированный XML документ (SoapFault). Если скрипт выводит бинарные данные, например изображение, то удобней выводить ошибки через WildFire. Во всех этих ситуация нужно просто применить разные форматы вывода.
Для разных форматов будем создавать разные классы. Я для начала реализую два формата вывода exceptionHandlerOutputWeb(для веба) и exceptionHandlerOutputCli(для командной строки). Так же нам понадобиться класс фабрика (exceptionHandlerOutputFactory), в котором будет инкапсулирована логика, когда какой формат вывода применить.

  1. public function getExceptionHandlerOutput(){
  2.     if(php_sapi_name() == ‘cli’){
  3.         return new exceptionHandlerOutputCli();
  4.     }
  5.     return new exceptionHandlerOutputWeb();
  6. }

При вызове setupHandlers можно установить формат вывода, передав экземпляр класса exceptionHandlerOutput* или exceptionHandlerOutputFactory*.

  1. exceptionHandlerClass::setupHandlers(new exceptionHandlerOutputAjax());

Благодаря такой архитектуре можно легко расширять форматы. Для создания нового формата достаточно создать класс, который будет наследоваться от абстрактного класса exceptionHandlerOutput и реализовать один метод (output).

  1. class exceptionHandlerOutputAjax extends exceptionHandlerOutput{
  2.     public function output($exception, $debug){
  3.         header(‘HTTP/1.0 500 Internal Server Error’, true, 500);
  4.         header(‘Status: 500 Internal Server Error’, true, 500);
  5.         $response = array(
  6.             ‘error’ => true,
  7.             ‘message’ => »,
  8.         );
  9.         if($debug){
  10.             $response[‘message’] = $exception->getMessage();
  11.         } else {
  12.             $response[‘message’] = self::$productionMessage;
  13.         }
  14.         exit(json_encode($response));
  15.     }
  16. }

Если нужна более сложная логика для автоматического выбора формата вывода, нужно создать класс, наследуемый от exceptionHandlerOutputFactory и реализовать метод getExceptionHandlerOutput.

  1. class exceptionHandlerOutputAjaxFactory extends exceptionHandlerOutputDefaultFactory{
  2.     public function getExceptionHandlerOutput() {
  3.         if( self::detect() ){
  4.             return new exceptionHandlerOutputAjax();
  5.         }
  6.         parent::getExceptionHandlerOutput();
  7.     }
  8.     public static function detect(){
  9.         return (!empty($_SERVER[‘HTTP_X_REQUESTED_WITH’]) 
  10.             && strtolower($_SERVER[‘HTTP_X_REQUESTED_WITH’]) == ‘xmlhttprequest’);
  11.     }
  12. }
  13. exceptionHandlerClass::setupHandlers(new exceptionHandlerOutputAjaxFactory());

Логирование

Как я и сказал выше логирование можно включать по желанию. Для этого в exceptionHandlerClass создан метод exceptionLog

  1. public static function exceptionLog($exception, $logPriority = null){
  2.     if(!is_null(self::$exceptionHandlerLog)){
  3.         self::$exceptionHandlerLog->log($exception, $logPriority);
  4.     }
  5. }

если нужно включить логирование, то достаточно сделать следующее:

  1. exceptionHandlerClass::$exceptionHandlerLog = new exceptionHandlerSimpleLog();

Класс для логирования должен наследоваться от абстрактного exceptionHandlerLog и реализовывать метод log

  1. class exceptionHandlerSimpleLog extends exceptionHandlerLog{
  2.     public function log($exception, $logType){
  3.         switch ($logType){
  4.             case self::uncaughtException:
  5.                 error_log($exception->getMessage());
  6.                 break;
  7.         }
  8.     }
  9. }

logType это одна из констант объявленных exceptionHandlerLog

  1. const uncaughtException = 0; //необработанные исключения
  2. const caughtException = 1; //вызов метода логирования вне обработчиков ошибок
  3. const ignoredError = 2; //ошибки маскированные @, логируются если выключена опция scream
  4. const lowPriorityError = 3; //ошибки которые не превращаются exception
  5. const assertion = 4; //assertion 

Имея logType и exception разработчик может сам решить какие искллючения и как логировать. Например, uncaughtException можно высылать по почте, ignoredError с severity E_ERROR логировать в файл итп.

Trace

При выводе trace я хочу видеть тип исключения, сообщение и собственно сам trace. В trace для каждого вызова должно выводится, какая функция вызвалась, список «кратких» параметров, файл и строка где произошел вызов. Что такое «краткие» параметры объясню на примерах: если функцию вызвали со строкой длиной в 1000 символов, то наличие этой строки в trace ничем не поможет при решении проблемы, а только затруднит чтение trace, это же касается массивов с большой вложенностью. Вывод trace на экран просто должен подсказать, где искать. Чтобы разобраться, что именно происходит нужно дебажить с помощью xdebug или примитивных var_dump() и die(), кому как больше нравится.
Пример trace:

	[ErrorException]: E_WARNING - mysql_connect(): Can't connect to MySQL server on 'localhost' (10061)
	#0: mysql_connect()
	    D:projects1dindex.php:19
	#1: testClass::test1("длиная строка…eeeeeery long string"(56))
	    D:projects1dindex.php:22
	#2: testClass->test2(testClass(), -∞, i:iTest, c:testClass, fa:testClass::test2)
	    D:projects1dindex.php:27
	#3: testAll(r:stream, fs:testClass::test1)
	    D:projects1dindex.php:30

Легенда

  • r: — resource
  • fs: — function (callable string)
  • fa: — function (callable array)
  • i: — interface (string)
  • c: — class (string)
  • ∞/INF — infinity
  • testClass() — object of type testClass
  • «»(n) — string, в скобках указана длина, … — место где вырезана часть строки
  • array(n) — array, в скобках указана длина

И самое полезное… ссылки на открытие файлов в IDE прямо из trace.

При нажатии на ссылку в IDE откроется соответствующий файл на соответствующей строке.
Для консольного режима (консоль NetBeans):
NetBeans Console

  1. exceptionHandlerOutputCli::setFileLinkFormat(‘: in %f on line %l’);

Для веб режима (TextMate):

  1. exceptionHandlerOutputWeb::setFileLinkFormat(‘txmt://open/?file://%f&line=%l’);

Можно реализовать для NetbBeans (или другого IDE). Для этого нужно: зарегистрировать протокол; сделать обработчик этого протокола (самое простое — bat файл). В обработчике через командную строку вызвать NetBeans с соответствующим файлом и строкой. Но это тема для следующей статьи.
Код писался за два дня так, что возможны мелкие недочеты. Скачать (не было времени, чтобы выложить в репозиторий).

UPD: перенесено в блог PHP
UPD2: в продолжение темы работа с исключениями в PHP

Исключения

Содержание

  • Наследование исключений

В PHP реализована модель исключений, аналогичная тем, что используются в других языках программирования.
Исключение в PHP может быть выброшено (throw) и поймано (catch).
Код может быть заключён в блок try, чтобы облегчить обработку потенциальных исключений.
У каждого блока try должен быть как минимум один соответствующий блок catch или finally.

Если выброшено исключение, а в текущей области видимости функции нет блока catch,
исключение будет «подниматься» по стеку вызовов к вызывающей функции, пока не найдёт подходящий блок catch.
Все блоки finally, которые встретятся на этом пути, будут выполнены.
Если стек вызовов разворачивается до глобальной области видимости, не встречая подходящего блока catch,
программа завершается с неисправимой ошибкой, если не был установлен глобальный обработчик исключений.

Выброшенный объект должен наследовать (instanceof) интерфейс Throwable.
Попытка выбросить объект, который таковым не является, приведёт к неисправимой ошибке PHP.

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

catch

Блок catch определяет, как реагировать на выброшенное исключение.
Блок catch определяет один или несколько типов исключений или ошибок, которые он может обработать,
и, по желанию, переменную, которой можно присвоить исключение
(указание переменной было обязательно до версии PHP 8.0.0).
Первый блок catch, с которым столкнётся выброшенное исключение или ошибка
и соответствует типу выброшенного объекта, обработает объект.

Несколько блоков catch могут быть использованы для перехвата различных классов исключений.
Нормальное выполнение (когда исключение не выброшено в блоке try)
будет продолжаться после последнего блока catch, определённого в последовательности.
Исключения могут быть выброшены (throw) (или повторно выброшены) внутри блока catch.
В противном случае выполнение будет продолжено после блока catch, который был вызван.

При возникновении исключения, код, следующий за утверждением, не будет выполнен,
а PHP попытается найти первый подходящий блок catch.
Если исключение не поймано, будет выдана неисправимая ошибка PHP
с сообщением «Uncaught Exception ...«,
если только обработчик не был определён с помощью функции set_exception_handler().

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

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

finally

Блок finally также может быть указан после или вместо блоков catch.
Код в блоке finally всегда будет выполняться после блоков try и catch,
независимо от того, было ли выброшено исключение
и до возобновления нормального выполнения.

Одно из заметных взаимодействий происходит между блоком finally и оператором return.
Если оператор return встречается внутри блоков try или catch, блок finally
всё равно будет выполнен. Более того, оператор return выполнится, когда встретится,
но результат будет возвращён после выполнения блока finally.
Кроме того, если блок finally также содержит оператор return,
возвращается значение из блока finally.

Глобальный обработчик исключений

Если исключению разрешено распространяться на глобальную область видимости,
оно может быть перехвачено глобальным обработчиком исключений, если он установлен.
Функция set_exception_handler() может задать функцию,
которая будет вызвана вместо блока catch, если не будет вызван никакой другой блок.
Эффект по сути такой же, как если бы вся программа была обёрнута в блок trycatch
с этой функцией в качестве catch.

Примечания

Замечание:

Внутренние функции PHP в основном используют отчёт об ошибках,
только современные объектно-ориентированные модули используют исключения.
Однако ошибки можно легко перевести в исключения с помощью класса ErrorException.
Однако эта техника работает только с исправляемыми ошибками.

Пример #1 Преобразование отчётов об ошибках в исключения


<?php
function exceptions_error_handler($severity, $message, $filename, $lineno) {
throw new
ErrorException($message, 0, $severity, $filename, $lineno);
}
set_error_handler('exceptions_error_handler');
?>

Примеры

Пример #2 Выбрасывание исключения


<?php
function inverse($x) {
if (!
$x) {
throw new
Exception('Деление на ноль.');
}
return
1/$x;
}

try {
echo

inverse(5) . "n";
echo
inverse(0) . "n";
} catch (
Exception $e) {
echo
'Выброшено исключение: ', $e->getMessage(), "n";
}
// Продолжение выполнения
echo "Привет, мирn";
?>

Результат выполнения данного примера:

0.2
Выброшено исключение: Деление на ноль.
Привет, мир

Пример #3 Обработка исключений с помощью блока finally


<?php
function inverse($x) {
if (!
$x) {
throw new
Exception('Деление на ноль.');
}
return
1/$x;
}

try {
echo

inverse(5) . "n";
} catch (
Exception $e) {
echo
'Поймано исключение: ', $e->getMessage(), "n";
} finally {
echo
"Первый блок finally.n";
}

try {
echo

inverse(0) . "n";
} catch (
Exception $e) {
echo
'Поймано исключение: ', $e->getMessage(), "n";
} finally {
echo
"Второй блок finally.n";
}
// Продолжение нормального выполнения
echo "Привет, мирn";
?>

Результат выполнения данного примера:

0.2
Первый блок finally.
Поймано исключение: Деление на ноль.
Второй блок finally.
Привет, мир

Пример #4 Взаимодействие между блоками finally и return


<?phpfunction test() {
try {
throw new
Exception('foo');
} catch (
Exception $e) {
return
'catch';
} finally {
return
'finally';
}
}

echo

test();
?>

Результат выполнения данного примера:

Пример #5 Вложенные исключения


<?phpclass MyException extends Exception { }

class

Test {
public function
testing() {
try {
try {
throw new
MyException('foo!');
} catch (
MyException $e) {
// повторный выброс исключения
throw $e;
}
} catch (
Exception $e) {
var_dump($e->getMessage());
}
}
}
$foo = new Test;
$foo->testing();?>

Результат выполнения данного примера:

Пример #6 Обработка нескольких исключений в одном блоке catch


<?phpclass MyException extends Exception { }

class

MyOtherException extends Exception { }

class

Test {
public function
testing() {
try {
throw new
MyException();
} catch (
MyException | MyOtherException $e) {
var_dump(get_class($e));
}
}
}
$foo = new Test;
$foo->testing();?>

Результат выполнения данного примера:

Пример #7 Пример блока catch без указания переменной

Допустимо начиная с PHP 8.0.0


<?phpclass SpecificException extends Exception {}

function

test() {
throw new
SpecificException('Ой!');
}

try {

test();
} catch (
SpecificException) {
print
"Было поймано исключение SpecificException, но нам безразлично, что у него внутри.";
}
?>

Пример #8 Throw как выражение

Допустимо начиная с PHP 8.0.0


<?phpclass SpecificException extends Exception {}

function

test() {
do_something_risky() or throw new Exception('Всё сломалось');
}

try {

test();
} catch (
Exception $e) {
print
$e->getMessage();
}
?>

ask at nilpo dot com

13 years ago


If you intend on creating a lot of custom exceptions, you may find this code useful.  I've created an interface and an abstract exception class that ensures that all parts of the built-in Exception class are preserved in child classes.  It also properly pushes all information back to the parent constructor ensuring that nothing is lost.  This allows you to quickly create new exceptions on the fly.  It also overrides the default __toString method with a more thorough one.

<?php
interface IException
{
   
/* Protected methods inherited from Exception class */
   
public function getMessage();                 // Exception message
   
public function getCode();                    // User-defined Exception code
   
public function getFile();                    // Source filename
   
public function getLine();                    // Source line
   
public function getTrace();                   // An array of the backtrace()
   
public function getTraceAsString();           // Formated string of trace

        /* Overrideable methods inherited from Exception class */

public function __toString();                 // formated string for display
   
public function __construct($message = null, $code = 0);
}

abstract class

CustomException extends Exception implements IException
{
    protected
$message = 'Unknown exception';     // Exception message
   
private   $string;                            // Unknown
   
protected $code    = 0;                       // User-defined exception code
   
protected $file;                              // Source filename of exception
   
protected $line;                              // Source line of exception
   
private   $trace;                             // Unknownpublic function __construct($message = null, $code = 0)
    {
        if (!
$message) {
            throw new
$this('Unknown '. get_class($this));
        }
       
parent::__construct($message, $code);
    }

        public function

__toString()
    {
        return
get_class($this) . " '{$this->message}' in {$this->file}({$this->line})n"
                               
. "{$this->getTraceAsString()}";
    }
}
?>

Now you can create new exceptions in one line:

<?php
class TestException extends CustomException {}
?>

Here's a test that shows that all information is properly preserved throughout the backtrace.

<?php
function exceptionTest()
{
    try {
        throw new
TestException();
    }
    catch (
TestException $e) {
        echo
"Caught TestException ('{$e->getMessage()}')n{$e}n";
    }
    catch (
Exception $e) {
        echo
"Caught Exception ('{$e->getMessage()}')n{$e}n";
    }
}

echo

'<pre>' . exceptionTest() . '</pre>';
?>

Here's a sample output:

Caught TestException ('Unknown TestException')
TestException 'Unknown TestException' in C:xampphtdocsCustomExceptionCustomException.php(31)
#0 C:xampphtdocsCustomExceptionExceptionTest.php(19): CustomException->__construct()
#1 C:xampphtdocsCustomExceptionExceptionTest.php(43): exceptionTest()
#2 {main}

Johan

11 years ago


Custom error handling on entire pages can avoid half rendered pages for the users:

<?php
ob_start
();
try {
   
/*contains all page logic
    and throws error if needed*/
   
...
} catch (
Exception $e) {
 
ob_end_clean();
 
displayErrorPage($e->getMessage());
}
?>

christof+php[AT]insypro.com

5 years ago


In case your E_WARNING type of errors aren't catchable with try/catch you can change them to another type of error like this:

<?php
    set_error_handler
(function($errno, $errstr, $errfile, $errline){
            if(
$errno === E_WARNING){
               
// make it more serious than a warning so it can be caught
               
trigger_error($errstr, E_ERROR);
                return
true;
            } else {
               
// fallback to default php error handler
               
return false;
            }
    });

    try {

// code that might result in a E_WARNING
   
} catch(Exception $e){
           
// code to handle the E_WARNING (it's actually changed to E_ERROR at this point)
   
} finally {
           
restore_error_handler();
    }
?>

Shot (Piotr Szotkowski)

14 years ago


‘Normal execution (when no exception is thrown within the try block, *or when a catch matching the thrown exception’s class is not present*) will continue after that last catch block defined in sequence.’

‘If an exception is not caught, a PHP Fatal Error will be issued with an “Uncaught Exception …” message, unless a handler has been defined with set_exception_handler().’

These two sentences seem a bit contradicting about what happens ‘when a catch matching the thrown exception’s class is not present’ (and the second sentence is actually correct).

Simo

7 years ago


#3 is not a good example. inverse("0a") would not be caught since (bool) "0a" returns true, yet 1/"0a" casts the string to integer zero and attempts to perform the calculation.

Edu

9 years ago


The "finally" block can change the exception that has been throw by the catch block.

<?php
try{
        try {
                throw new
Exception("Hello");
        } catch(
Exception $e) {
                echo
$e->getMessage()." catch inn";
                throw
$e;
        } finally {
                echo
$e->getMessage()." finally n";
                throw new
Exception("Bye");
        }
} catch (
Exception $e) {
        echo
$e->getMessage()." catch outn";
}
?>

The output is:

Hello catch in
Hello finally
Bye catch out

daviddlowe dot flimm at gmail dot com

5 years ago


Starting in PHP 7, the classes Exception and Error both implement the Throwable interface. This means, if you want to catch both Error instances and Exception instances, you should catch Throwable objects, like this:

<?phptry {
    throw new
Error( "foobar" );
   
// or:
    // throw new Exception( "foobar" );
}
catch (
Throwable $e) {
   
var_export( $e );
}
?>

mlaopane at gmail dot com

4 years ago


<?php/**
* You can catch exceptions thrown in a deep level function
*/
function employee()
{
    throw new
Exception("I am just an employee !");
}

function

manager()
{
   
employee();
}

function

boss()
{
    try {
       
manager();
    } catch (
Exception $e) {
        echo
$e->getMessage();
    }
}
boss(); // output: "I am just an employee !"

telefoontoestel at nospam dot org

8 years ago


When using finally keep in mind that when a exit/die statement is used in the catch block it will NOT go through the finally block.

<?php
try {
    echo
"try block<br />";
    throw new
Exception("test");
} catch (
Exception $ex) {
    echo
"catch block<br />";
} finally {
    echo
"finally block<br />";
}
// try block
// catch block
// finally block
?>

<?php
try {
    echo
"try block<br />";
    throw new
Exception("test");
} catch (
Exception $ex) {
    echo
"catch block<br />";
    exit(
1);
} finally {
    echo
"finally block<br />";
}
// try block
// catch block
?>

Tom Polomsk

8 years ago


Contrary to the documentation it is possible in PHP 5.5 and higher use only try-finally blocks without any catch block.

Sawsan

11 years ago


the following is an example of a re-thrown exception and the using of getPrevious function:

<?php

$name

= "Name";//check if the name contains only letters, and does not contain the word nametry
   {
   try
     {
      if (
preg_match('/[^a-z]/i', $name))
       {
           throw new
Exception("$name contains character other than a-z A-Z");
       }  
       if(
strpos(strtolower($name), 'name') !== FALSE)
       {
          throw new
Exception("$name contains the word name");
       }
       echo
"The Name is valid";
     }
   catch(
Exception $e)
     {
     throw new
Exception("insert name again",0,$e);
     }
   }

catch (

Exception $e)
   {
   if (
$e->getPrevious())
   {
    echo
"The Previous Exception is: ".$e->getPrevious()->getMessage()."<br/>";
   }
   echo
"The Exception is: ".$e->getMessage()."<br/>";
   }
?>

lscorionjs at gmail dot com

14 days ago


<?phptry {
 
$str = 'hi';
  throw new
Exception();
} catch (
Exception) {
 
var_dump($str);
} finally {
 
var_dump($str);
}
?>

Output:
string(2) "hi"
string(2) "hi"

ilia-yats at ukr dot net

1 month ago


Note some undocumented details about exceptions thrown from 'finally' blocks.

When exception is thrown from 'finally' block, it overrides the original not-caught (or re-thrown) exception. So the behavior is similar to 'return': value returned from 'finally' overrides the one returned earlier. And the original exception is automatically appended to the exceptions chain, i.e. becomes 'previous' for the new one. Example:
<?php
try {
    try {
        throw new
Exception('thrown from try');
    } finally {
        throw new
Exception('thrown from finally');
    }
} catch(
Exception $e) {
    echo
$e->getMessage();
    echo
PHP_EOL;
    echo
$e->getPrevious()->getMessage();
}
// will output:
// thrown from finally
// thrown from try
?>

Example with re-throwing:
<?php
try {
    try {
        throw new
Exception('thrown from try');
    } catch (
Exception $e) {
        throw new
Exception('thrown from catch');
    } finally {
        throw new
Exception('thrown from finally');
    }
} catch(
Exception $e) {
    echo
$e->getMessage();
    echo
PHP_EOL;
    echo
$e->getPrevious()->getMessage();
}
// will output:
// thrown from finally
// thrown from catch
?>

The same happens even if explicitly pass null as previous exception:
<?php
try {
    try {
        throw new
Exception('thrown from try');
    } finally {
        throw new
Exception('thrown from finally', null, null);
    }
} catch(
Exception $e) {
    echo
$e->getMessage();
    echo
PHP_EOL;
    echo
$e->getPrevious()->getMessage();
}
// will output:
// thrown from finally
// thrown from try
?>

Also it is possible to pass previous exception explicitly, the 'original' one will be still appended to the chain, e.g.:
<?php
try {
    try {
        throw new
Exception('thrown from try');
    } finally {
        throw new
Exception(
           
'thrown from finally',
           
null,
            new
Exception('Explicitly set previous!')
        );
    }
} catch(
Exception $e) {
    echo
$e->getMessage();
    echo
PHP_EOL;
    echo
$e->getPrevious()->getMessage();
    echo
PHP_EOL;
    echo
$e->getPrevious()->getPrevious()->getMessage();
}
// will output:
// thrown from finally
// Explicitly set previous!
// thrown from try
?>

This seems to be true for versions 5.6-8.2.

Daan

1 year ago


I would like to emphasise that you can not rethrow an Exception inside a catch-block and expect that the next catch-block will handle it.

<?php try {
    throw new
RuntimeException('error');           
} catch (
RuntimeException $e) {
    throw
$e;
} catch (
Exception $e) {
   
// this will not be executed[
}
?>

Антон Шевчук // Web-разработчик

Не совершает ошибок только тот, кто ничего не делает, и мы тому пример – трудимся не покладая рук над созданием рабочих мест для тестировщиков :)

О да, в этой статье я поведу свой рассказа об ошибках в PHP, и том как их обуздать.

Ошибки

Разновидности в семействе ошибок

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

Чтобы ни одна ошибка не ушла незамеченной потребуется включить отслеживание всех ошибок с помощью функции error_reporting(), а с помощью директивы display_errors включить их отображение:

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

Фатальные ошибки

Самый грозный вид ошибок – фатальные, они могут возникнуть как при компиляции, так и при работе парсера или PHP-скрипта, выполнение скрипта при этом прерывается.

E_PARSE
Это ошибка появляется, когда вы допускаете грубую ошибку синтаксиса и интерпретатор PHP не понимает, что вы от него хотите, например если не закрыли фигурную или круглую скобочку:

<?php
/**
 Parse error: syntax error, unexpected end of file
 */
{

Или написали на непонятном языке:

<?php
/**
 Parse error: syntax error, unexpected '...' (T_STRING)
 */
Тут будет ошибка парсера

Лишние скобочки тоже встречаются, и не важно круглые либо фигурные:

<?php
/**
 Parse error: syntax error, unexpected '}'
 */
}

Отмечу один важный момент – код файла, в котором вы допустили parse error не будет выполнен, следовательно, если вы попытаетесь включить отображение ошибок в том же файле, где возникла ошибка парсера то это не сработает:

<?php
// этот код не сработает
error_reporting(E_ALL);
ini_set('display_errors', 1);

// т.к. вот тут
ошибка парсера

E_ERROR
Это ошибка появляется, когда PHP понял что вы хотите, но сделать сие не получилось ввиду ряда причин, так же прерывает выполнение скрипта, при этом код до появления ошибки сработает:

Не был найден подключаемый файл:

/**
 Fatal error: require_once(): Failed opening required 'not-exists.php' (include_path='.:/usr/share/php:/usr/share/pear')
 */
require_once 'not-exists.php';

Было брошено исключение (что это за зверь, расскажу немного погодя), но не было обработано:

/**
 Fatal error: Uncaught exception 'Exception'
 */
throw new Exception();

При попытке вызвать несуществующий метод класса:

/**
 Fatal error: Call to undefined method stdClass::notExists()
 */
$stdClass = new stdClass();
$stdClass->notExists();

Отсутствия свободной памяти (больше, чем прописано в директиве memory_limit) или ещё чего-нить подобного:

/**
 Fatal Error: Allowed Memory Size
 */
$arr = array();

while (true) {
    $arr[] = str_pad(' ', 1024);
}

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

Рекурсивный вызов функции. В данном примере он закончился на 256-ой итерации, ибо так прописано в настройках xdebug:

/**
 Fatal error: Maximum function nesting level of '256' reached, aborting!
 */
function deep() {
    deep();
}
deep();

Не фатальные

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

E_WARNING
Частенько встречается, когда подключаешь файл с использованием include, а его не оказывается на сервере или ошиблись указывая путь к файлу:

/**
 Warning: include_once(): Failed opening 'not-exists.php' for inclusion
 */
include_once 'not-exists.php';

Бывает, если используешь неправильный тип аргументов при вызове функций:

/**
 Warning: join(): Invalid arguments passed
 */
join('string', 'string');

Их очень много, и перечислять все не имеет смысла…

E_NOTICE
Это самые распространенные ошибки, мало того, есть любители отключать вывод ошибок и клепают их целыми днями. Возникают при целом ряде тривиальных ошибок.

Когда обращаются к неопределенной переменной:

/**
 Notice: Undefined variable: a
 */
echo $a;

Когда обращаются к несуществующему элементу массива:

<?php
/**
 Notice: Undefined index: a
 */
$b = array();
$b['a'];

Когда обращаются к несуществующей константе:

/**
 Notice: Use of undefined constant UNKNOWN_CONSTANT - assumed 'UNKNOWN_CONSTANT'
 */
echo UNKNOWN_CONSTANT;

Когда не конвертируют типы данных:

/**
 Notice: Array to string conversion
 */
echo array();

Для избежания подобных ошибок – будьте внимательней, и если вам IDE подсказывает о чём-то – не игнорируйте её:

PHP E_NOTICE in PHPStorm

E_STRICT
Это ошибки, которые научат вас писать код правильно, чтобы не было стыдно, тем более IDE вам эти ошибки сразу показывают. Вот например, если вызвали не статический метод как статику, то код будет работать, но это как-то неправильно, и возможно появление серьёзных ошибок, если в дальнейшем метод класса будет изменён, и появится обращение к $this:

/**
 Strict standards: Non-static method Strict::test() should not be called statically
 */
class Strict { 
    public function test() { 
        echo 'Test'; 
    } 
}

Strict::test();

E_DEPRECATED
Так PHP будет ругаться, если вы используете устаревшие функции (т.е. те, что помечены как deprecated, и в следующем мажорном релизе их не будет):

/**
 Deprecated: Function split() is deprecated
 */

// популярная функция, всё никак не удалят из PHP
// deprecated since 5.3
split(',', 'a,b');

В моём редакторе подобные функции будут зачёркнуты:

PHP E_DEPRECATED in PHPStorm

Обрабатываемые

Этот вид, которые разводит сам разработчик кода, я их уже давно не встречал, не рекомендую их вам заводить:

  • E_USER_ERROR – критическая ошибка
  • E_USER_WARNING – не критическая ошибка
  • E_USER_NOTICE – сообщения которые не являются ошибками

Отдельно стоит отметить E_USER_DEPRECATED – этот вид всё ещё используется очень часто для того, чтобы напомнить программисту, что метод или функция устарели и пора переписать код без использования оной. Для создания этой и подобных ошибок используется функция trigger_error():

/**
 * @deprecated Deprecated since version 1.2, to be removed in 2.0
 */
function generateToken() {
    trigger_error('Function `generateToken` is deprecated, use class `Token` instead', E_USER_DEPRECATED);
    // ...
    // code ...
    // ...
}

Теперь, когда вы познакомились с большинством видов и типов ошибок, пора озвучить небольшое пояснение по работе директивы display_errors:

  • если display_errors = on, то в случае ошибки браузер получит html c текстом ошибки и кодом 200
  • если же display_errors = off, то для фатальных ошибок код ответа будет 500 и результат не будет возвращён пользователю, для остальных ошибок – код будет работать неправильно, но никому об этом не расскажет

Приручение

Для работы с ошибками в PHP существует 3 функции:

  • set_error_handler() — устанавливает обработчик для ошибок, которые не обрывают работу скрипта (т.е. для не фатальных ошибок)
  • error_get_last() — получает информацию о последней ошибке
  • register_shutdown_function() — регистрирует обработчик который будет запущен при завершении работы скрипта. Данная функция не относится непосредственно к обработчикам ошибок, но зачастую используется именно для этого

Теперь немного подробностей об обработке ошибок с использованием set_error_handler(), в качестве аргументов данная функция принимает имя функции, на которую будет возложена миссия по обработке ошибок и типы ошибок которые будут отслеживаться. Обработчиком ошибок может так же быть методом класса, или анонимной функцией, главное, чтобы он принимал следующий список аргументов:

  • $errno – первый аргумент содержит тип ошибки в виде целого числа
  • $errstr – второй аргумент содержит сообщение об ошибке
  • $errfile – необязательный третий аргумент содержит имя файла, в котором произошла ошибка
  • $errline – необязательный четвертый аргумент содержит номер строки, в которой произошла ошибка
  • $errcontext – необязательный пятый аргумент содержит массив всех переменных, существующих в области видимости, где произошла ошибка

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

<?php
    // включаем отображение всех ошибок, кроме E_NOTICE
    error_reporting(E_ALL & ~E_NOTICE);
    ini_set('display_errors', 1);
    
    // наш обработчик ошибок
    function myHandler($level, $message, $file, $line, $context) {
        // в зависимости от типа ошибки формируем заголовок сообщения
        switch ($level) {
            case E_WARNING:
                $type = 'Warning';
                break;
            case E_NOTICE:
                $type = 'Notice';
                break;
            default;
                // это не E_WARNING и не E_NOTICE
                // значит мы прекращаем обработку ошибки
                // далее обработка ложится на сам PHP
                return false;
        }
        // выводим текст ошибки
        echo "<h2>$type: $message</h2>";
        echo "<p><strong>File</strong>: $file:$line</p>";
        echo "<p><strong>Context</strong>: $". join(', $', array_keys($context))."</p>";
        // сообщаем, что мы обработали ошибку, и дальнейшая обработка не требуется
        return true;
    }
    
    // регистрируем наш обработчик, он будет срабатывать на для всех типов ошибок
    set_error_handler('myHandler', E_ALL);

У вас не получится назначить более одной функции для обработки ошибок, хотя очень бы хотелось регистрировать для каждого типа ошибок свой обработчик, но нет – пишите один обработчик, и всю логику отображения для каждого типа описывайте уже непосредственно в нём

С обработчиком, который написан выше есть одна существенная проблема – он не ловит фатальные ошибки, и вместо сайта пользователи увидят лишь пустую страницу, либо, что ещё хуже, сообщение об ошибке. Дабы не допустить подобного сценария следует воспользоваться функцией register_shutdown_function() и с её помощью зарегистрировать функцию, которая всегда будет выполняться по окончанию работы скрипта:

function shutdown() {
    echo 'Этот текст будет всегда отображаться';
}
register_shutdown_function('shutdown');

Данная функция будет срабатывать всегда!

Но вернёмся к ошибкам, для отслеживания появления в коде ошибки воспользуемся функцией error_get_last(), с её помощью можно получить информацию о последней выявленной ошибке, а поскольку фатальные ошибки прерывают выполнение кода, то они всегда будут выполнять роль “последних”:

function shutdown() {
    $error = error_get_last();
    if (
        // если в коде была допущена ошибка
        is_array($error) &&
        // и это одна из фатальных ошибок
        in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])
    ) {
        // очищаем буфер вывода (о нём мы ещё поговорим в последующих статьях)
        while (ob_get_level()) {
            ob_end_clean();
        }
        // выводим описание проблемы
        echo 'Сервер находится на техническом обслуживании, зайдите позже';
    }
}
register_shutdown_function('shutdown');

Задание
Дополнить обработчик фатальных ошибок выводом исходного кода файла где была допущена ошибка, а так же добавьте подсветку синтаксиса выводимого кода.

О прожорливости

Проведём простой тест, и выясним – сколько драгоценных ресурсов кушает самая тривиальная ошибка:

/**
 * Этот код не вызывает ошибок
 */

// сохраняем параметры памяти и времени выполнения скрипта
$memory = memory_get_usage();
$time= microtime(true);

$a = '';
$arr = [];
for ($i = 0; $i < 10000; $i++) {
    $arr[$a] = $i;
}

printf('%f seconds <br/>', microtime(true) - $time);
echo number_format(memory_get_usage() - $memory, 0, '.', ' '), ' bytes<br/>';

В результате запуска данного скрипта у меня получился вот такой результат:

0.002867 seconds 
984 bytes

Теперь добавим ошибку в цикле:

/**
 * Этот код содержит ошибку
 */

// сохраняем параметры памяти и времени выполнения скрипта
$memory = memory_get_usage();
$time= microtime(true);

$a = '';
$arr = [];
for ($i = 0; $i < 10000; $i++) {
    $arr[$b] = $i; // тут ошиблись с именем переменной
}

printf('%f seconds <br/>', microtime(true) - $time);
echo number_format(memory_get_usage() - $memory, 0, '.', ' '), ' bytes<br/>';

Результат ожидаемо хуже, и на порядок (даже на два порядка!):

0.263645 seconds 
992 bytes

Вывод однозначен – ошибки в коде приводят к лишней прожорливости скриптов – так что во время разработки и тестирования приложения включайте отображение всех ошибок!

Тестирование проводил на PHP версии 5.6, в седьмой версии результат лучше – 0.0004 секунды против 0.0050 – разница только на один порядок, но в любом случае результат стоит прикладываемых усилий по исправлению ошибок

Где собака зарыта

В PHP есть спец символ «@» – оператор подавления ошибок, его используют дабы не писать обработку ошибок, а положится на корректное поведение PHP в случае чего:

<?php
    echo @UNKNOWN_CONSTANT;

При этом обработчик ошибок указанный в set_error_handler() всё равно будет вызван, а факт того, что к ошибке было применено подавление можно отследить вызвав функцию error_reporting() внутри обработчика, в этом случае она вернёт 0.

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

Исключения

В эру PHP4 не было исключений (exceptions), всё было намного сложнее, и разработчики боролись с ошибками как могли, это было сражение не на жизнь, а на смерть… Окунуться в эту увлекательную историю противостояния можете в статье Исключительный код. Часть 1. Стоит ли её читать сейчас? Думаю да, ведь это поможет вам понять эволюцию языка, и раскроет всю прелесть исключений

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

К примеру, скрипт должен сохранить какие-то данные в кеш файл, если что-то пошло не так (нет доступа на запись, нет места на диске), генерируется исключение соответствующего типа, а в обработчике исключений принимается решение – сохранить в другое место или сообщить пользователю о проблеме.

Исключение – это объект который наследуется от класса Exception, содержит текст ошибки, статус, а также может содержать ссылку на другое исключение которое стало первопричиной данного. Модель исключений в PHP схожа с используемыми в других языках программирования. Исключение можно инициировать (как говорят, “бросить”) при помощи оператора throw, и можно перехватить (“поймать”) оператором catch. Код генерирующий исключение, должен быть окружен блоком try, для того чтобы можно было перехватить исключение. Каждый блок try должен иметь как минимум один соответствующий ему блок catch или finally:

try {
    // код который может выбросить исключение
    if (rand(0, 1)) {
        throw new Exception('One')
    } else {
        echo 'Zero';
    }
} catch (Exception $e) {
    // код который может обработать исключение
    echo $e->getMessage();
}

В каких случаях стоит применять исключения:

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

Для иллюстрации первого сценария возьмём уже озвученный пример функции для записи данных в файл – помешать нам может очень много факторов, а для того, чтобы сообщить выше стоящему коду в чем именно была проблема необходимо создать и выбросить исключение:

$directory = __DIR__ . DIRECTORY_SEPARATOR . 'logs';

// директории может не быть
if (!is_dir($directory)) {
    throw new Exception('Directory `logs` is not exists');
}

// может не быть прав на запись в директорию
if (!is_writable($directory)) {
    throw new Exception('Directory `logs` is not writable');
}

// возможно кто-то уже создал файл, и закрыл к нему доступ
if (!$file = @fopen($directory . DIRECTORY_SEPARATOR . date('Y-m-d') . '.log', 'a+')) {
    throw new Exception('System can't create log file');
}

fputs($file, date('[H:i:s]') . " donen");
fclose($file);

Соответственно ловить данные исключения будем примерно так:

try {
    // код который пишет в файл
    // ...
} catch (Exception $e) {
    // выводим текст ошибки
    echo 'Не получилось: '. $e->getMessage();
}

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

// исключения файловой системы
class FileSystemException extends Exception {}

// исключения связанные с директориями
class DirectoryException extends FileSystemException {
    // коды исключений
    const DIRECTORY_NOT_EXISTS =  1;
    const DIRECTORY_NOT_WRITABLE = 2;
}

// исключения связанные с файлами
class FileException extends FileSystemException {}

Теперь, если использовать эти исключения то можно получить следующий код:

try {
    // код который пишет в файл
    if (!is_dir($directory)) {
        throw new DirectoryException('Directory `logs` is not exists', DirectoryException::DIRECTORY_NOT_EXISTS);
    }

    if (!is_writable($directory)) {
        throw new DirectoryException('Directory `logs` is not writable', DirectoryException::DIRECTORY_NOT_WRITABLE);
    }

    if (!$file = @fopen($directory . DIRECTORY_SEPARATOR . date('Y-m-d') . '.log', 'a+')) {
        throw new FileException('System can't open log file');
    }

    fputs($file, date('[H:i:s]'') . " donen");
    fclose($file);
} catch (DirectoryException $e) {
    echo 'С директорией возникла проблема: '. $e->getMessage();
} catch (FileException $e) {
    echo 'С файлом возникла проблема: '. $e->getMessage();
} catch (FileSystemException $e) {
    echo 'Ошибка файловой системы: '. $e->getMessage();
} catch (Exception $e) {
    echo 'Ошибка сервера: '. $e->getMessage();
}

Важно помнить, что Exception — это прежде всего исключительное событие, иными словами исключение из правил. Не нужно использовать их для обработки очевидных ошибок, к примеру, для валидации введённых пользователем данных (хотя тут не всё так однозначно). При этом обработчик исключений должен быть написан в том месте, где он будет способен его обработать. К примеру, обработчик для исключений вызванных недоступностью файла для записи должен быть в методе, который отвечает за выбор файла или методе его вызывающем, для того что бы он имел возможность выбрать другой файл или другую директорию.

Так, а что будет если не поймать исключение? Вы получите “Fatal Error: Uncaught exception …”. Неприятно.
Чтобы избежать подобной ситуации следует использовать функцию set_exception_handler() и установить обработчик для исключений, которые брошены вне блока try-catch и не были обработаны. После вызова такого обработчика выполнение скрипта будет остановлено:

// в качестве обработчика событий 
// будем использовать анонимную функцию
set_exception_handler(function($exception) {
    /** @var Exception $exception */
    echo $exception->getMessage(), "<br/>n";
    echo $exception->getFile(), ':', $exception->getLine(), "<br/>n";
    echo $exception->getTraceAsString(), "<br/>n";
});

Ещё расскажу про конструкцию с использованием блока finally – этот блок будет выполнен вне зависимости от того, было выброшено исключение или нет:

try {
    // код который может выбросить исключение
} catch (Exception $e) {
    // код который может обработать исключение
    // если конечно оно появится
} finally {
    // код, который будет выполнен при любом раскладе
}

Для понимания того, что это нам даёт приведу следующий пример использования блока finally:

try {
    // где-то глубоко внутри кода
    // соединение с базой данных
    $handler = mysqli_connect('localhost', 'root', '', 'test');

    try {
        // при работе с БД возникла исключительная ситуация
        // ...
        throw new Exception('DB error');
    } catch (Exception $e) {
        // исключение поймали, обработали на своём уровне
        // и должны его пробросить вверх, для дальнейшей обработки
        throw new Exception('Catch exception', 0, $e);
    } finally {
        // но, соединение с БД необходимо закрыть
        // будем делать это в блоке finally
        mysqli_close($handler);
    }

    // этот код не будет выполнен, если произойдёт исключение в коде выше
    echo "Ok";
} catch (Exception $e) {
    // ловим исключение, и выводим текст
    echo $e->getMessage();
    echo "<br/>";
    // выводим информацию о первоначальном исключении
    echo $e->getPrevious()->getMessage();
}

Т.е. запомните – блок finally будет выполнен даже в том случае, если вы в блоке catch пробрасываете исключение выше (собственно именно так он и задумывался).

Для вводной статьи информации в самый раз, кто жаждет ещё подробностей, то вы их найдёте в статье Исключительный код ;)

Задание
Написать свой обработчик исключений, с выводом текста файла где произошла ошибка, и всё это с подсветкой синтаксиса, так же не забудьте вывести trace в читаемом виде. Для ориентира – посмотрите как это круто выглядит у whoops.

PHP7 – всё не так, как было раньше

Так, вот вы сейчас всю информацию выше усвоили и теперь я буду грузить вас нововведениями в PHP7, т.е. я буду рассказывать о том, с чем вы столкнётесь через год работы PHP разработчиком. Ранее я вам рассказывал и показывал на примерах какой костыль нужно соорудить, чтобы отлавливать критические ошибки, так вот – в PHP7 это решили исправить, но как обычно завязались на обратную совместимость кода, и получили хоть и универсальное решение, но оно далеко от идеала. А теперь по пунктам об изменениях:

  1. при возникновении фатальных ошибок типа E_ERROR или фатальных ошибок с возможностью обработки E_RECOVERABLE_ERROR PHP выбрасывает исключение
  2. эти исключения не наследуют класс Exception (помните я говорил об обратной совместимости, это всё ради неё)
  3. эти исключения наследуют класс Error
  4. оба класса Exception и Error реализуют интерфейс Throwable
  5. вы не можете реализовать интерфейс Throwable в своём коде

Интерфейс Throwable практически полностью повторяет нам Exception:

interface Throwable
{
    public function getMessage(): string;
    public function getCode(): int;
    public function getFile(): string;
    public function getLine(): int;
    public function getTrace(): array;
    public function getTraceAsString(): string;
    public function getPrevious(): Throwable;
    public function __toString(): string;
}

Сложно? Теперь на примерах, возьмём те, что были выше и слегка модернизируем:

try {
    // файл, который вызывает ошибку парсера
    include 'e_parse_include.php';
} catch (Error $e) {
    var_dump($e);
}

В результате ошибку поймаем и выведем:

object(ParseError)#1 (7) {
  ["message":protected] => string(48) "syntax error, unexpected 'будет' (T_STRING)"
  ["string":"Error":private] => string(0) ""
  ["code":protected] => int(0)
  ["file":protected] => string(49) "/www/education/error/e_parse_include.php"
  ["line":protected] => int(4)
  ["trace":"Error":private] => array(0) { }
  ["previous":"Error":private] => NULL
}

Как видите – поймали исключение ParseError, которое является наследником исключения Error, который реализует интерфейс Throwable, в доме который построил Джек. Ещё есть другие, но не буду мучать – для наглядности приведу иерархию исключений:

interface Throwable
  |- Exception implements Throwable
  |    |- ErrorException extends Exception
  |    |- ... extends Exception
  |    `- ... extends Exception
  `- Error implements Throwable
      |- TypeError extends Error
      |- ParseError extends Error
      |- ArithmeticError extends Error
      |  `- DivisionByZeroError extends ArithmeticError
      `- AssertionError extends Error 

TypeError – для ошибок, когда тип аргументов функции не совпадает с передаваемым типом:

try {
    (function(int $one, int $two) {
        return;
    })('one', 'two');
} catch (TypeError $e) {
    echo $e->getMessage();
}

ArithmeticError – могут возникнуть при математических операциях, к примеру когда результат вычисления превышает лимит выделенный для целого числа:

try {
    1 << -1;
} catch (ArithmeticError $e) {
    echo $e->getMessage();
}

DivisionByZeroError – ошибка деления на ноль:

try {
    1 / 0;
} catch (ArithmeticError $e) {
    echo $e->getMessage();
}

AssertionError – редкий зверь, появляется когда условие заданное в assert() не выполняется:

ini_set('zend.assertions', 1);
ini_set('assert.exception', 1);

try {
    assert(1 === 0);
} catch (AssertionError $e) {
    echo $e->getMessage();
}

При настройках production-серверов, директивы zend.assertions и assert.exception отключают, и это правильно

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

При написании данного раздела были использованы материалы из статьи Throwable Exceptions and Errors in PHP 7

Отладка

Иногда для отладки кода нужно отследить что происходило с переменной или объектом на определённом этапе, для этих целей есть функция debug_backtrace() и debug_print_backtrace() которые вернут историю вызовов функций/методов в обратном порядке:

<?php
function example() {
    echo '<pre>';
    debug_print_backtrace();
    echo '</pre>';
}

class ExampleClass {
    public static function method () {
        example();
    }
}

ExampleClass::method();

В результате выполнения функции debug_print_backtrace() будет выведен список вызовов приведших нас к данной точке:

#0  example() called at [/www/education/error/backtrace.php:10]
#1  ExampleClass::method() called at [/www/education/error/backtrace.php:14]

Проверить код на наличие синтаксических ошибок можно с помощью функции php_check_syntax() или же команды php -l [путь к файлу], но я не встречал использования оных.

Assert

Отдельно хочу рассказать о таком экзотическом звере как assert() в PHP, собственно это кусочек контрактной методологии программирования, и дальше я расскажу вам как я никогда его не использовал :)

Первый случай – это когда вам надо написать TODO прямо в коде, да так, чтобы точно не забыть реализовать заданный функционал:

// включаем вывод ошибок
error_reporting(E_ALL);
ini_set('display_errors', 1);

// включаем asserts
ini_set('zend.assertions', 1);
ini_set('assert.active', 1);

assert(false, "Remove it!");

В результате выполнения данного кода получим E_WARNING:

Warning: assert(): Remove it! failed

PHP7 можно переключить в режим exception, и вместо ошибки будет всегда появляться исключение AssertionError:

// включаем asserts
ini_set('zend.assertions', 1);
ini_set('assert.active', 1);
// переключаем на исключения
ini_set('assert.exception', 1);

assert(false, "Remove it!");

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

assert(false, new Exception("Remove it!"));

Но я бы рекомендовал использовать метки @TODO, современные IDE отлично с ними работают, и вам не нужно будет прикладывать дополнительные усилия и ресурсы для работы с ними

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

// callback-функция для вывода информации в браузер
function backlog($script, $line, $code, $message) {
    echo "<h3>$message</h3>";
    highlight_string ($code);
}

// устанавливаем callback-функцию
assert_options(ASSERT_CALLBACK, 'backlog');
// отключаем вывод предупреждений
assert_options(ASSERT_WARNING,  false);

// пишем проверку и её описание
assert("sqr(4) == 16", "When I send integer, function should return square of it");

// функция, которую проверяем
function sqr($a) {
    return; // она не работает
}

Третий теоретический вариант – это непосредственно контрактное программирование – когда вы описали правила использования своей библиотеки, но хотите точно убедится, что вас поняли правильно, и в случае чего сразу указать разработчику на ошибку (я вот даже не уверен, что правильно его понимаю, но пример кода вполне рабочий):

/**
 * Настройки соединения должны передаваться в следующем виде
 *
 *     [
 *         'host' => 'localhost',
 *         'port' => 3306,
 *         'name' => 'dbname',
 *         'user' => 'root',
 *         'pass' => ''
 *     ]
 *
 * @param $settings
 */
function setupDb ($settings) {
    // проверяем настройки
    assert(isset($settings['host']), 'Db `host` is required');
    assert(isset($settings['port']) && is_int($settings['port']), 'Db `port` is required, should be integer');
    assert(isset($settings['name']), 'Db `name` is required, should be integer');

    // соединяем с БД
    // ...
}

setupDb(['host' => 'localhost']);

Никогда не используйте assert() для проверки входных параметров, ведь фактически assert() интерпретирует строковую переменную (ведёт себя как eval()), а это чревато PHP-инъекцией. И да, это правильное поведение, т.к. просто отключив assert’ы всё что передаётся внутрь будет проигнорировано, а если делать как в примере выше, то код будет выполняться, а внутрь отключенного assert’a будет передан булевый результат выполнения

Если у вас есть живой опыт использования assert() – поделитесь со мной, буду благодарен. И да, вот вам ещё занимательно чтива по этой теме – PHP Assertions, с таким же вопросом в конце :)

В заключение

Я за вас напишу выводы из данной статьи:

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

P.S. Спасибо Максиму Слесаренко за помощь в написании статьи

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

  • Веб-серверу может не хватить места на диске;
  • Пользователь мог ввести недопустимое значение в поле формы;
  • Файл или запись базы данных, к которой вы пытались получить доступ, возможно, не существует;
  • Приложение может не иметь разрешения на запись в файл на диске;
  • Служба, к которой приложение должно получить доступ, может быть временно недоступна.

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

Профессиональное приложение должно иметь возможность изящно обрабатывать такие ошибки времени выполнения. Обычно это означает более четкое и точное информирование пользователя о проблеме.

Понимание уровней ошибок

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

Название Значение Описание
E_ERROR 1 Неустранимая ошибка времени выполнения от которой невозможно избавиться. Выполнение скрипта немедленно прекращается.
E_WARNING 2 Предупреждение во время выполнения. Она несущественна, и большинство ошибок попадают в эту категорию. Выполнение скрипта не останавливается.
E_NOTICE 8 Уведомление во время выполнения. Указывает, что скрипт обнаружил что-то, что могло быть ошибкой, хотя такая ситуация также может возникнуть при обычном запуске скрипта.
E_USER_ERROR 256 Сообщение о фатальной пользовательской ошибке. Она похожа на E_ERROR, за исключением того, что она генерируется PHP-скриптом с использованием функции trigger_error().
E_USER_WARNING 512 Предупреждающее сообщение, созданное пользователем без фатального исхода. Она похожа на E_WARNING, за исключением того, что она генерируется PHP-скриптом с использованием функции trigger_error().
E_USER_NOTICE 1024 Сообщение с уведомлением, созданное пользователем. Она похожа на E_NOTICE за исключением того, что она генерируется PHP-скриптом с использованием функции trigger_error().
E_STRICT 2048 Не совсем ошибка, но срабатывает всякий раз, когда PHP встречает код, который может привести к проблемам или несовместимости пересылки.
E_ALL 8191 Все ошибки и предупреждения, кроме E_STRICT до PHP 5.4.0.

Дополнительные сведения об уровнях ошибок см. в справочнике по уровням ошибок PHP.

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

Базовая обработка ошибок с помощью функции die()

Рассмотрим следующий пример, в котором просто попытаемся открыть текстовый файл только для чтения.

<?php
// Пробуем открыть несуществующий файл
$file = fopen("sample.txt", "r"); // Выводит: Warning: fopen(sample.txt) [function.fopen]: failed to open stream: No such file or directory in C:wampwwwprojecttest.php on line 2
?>

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

<?php
if(file_exists("sample.txt")){
    $file = fopen("sample.txt", "r");
} else{
    die("Error: The file you are trying to access doesn't exist.");
}
?>

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

Используемая выше функция die() просто отображает пользовательское сообщение об ошибке и завершает текущий скрипт, если файл sample.txt не найден.

Создание собственного обработчика ошибок

Вы можете создать свою собственную функцию обработчика ошибок, чтобы справляться с ошибкой времени выполнения, генерируемой механизмом PHP. Пользовательский обработчик ошибок обеспечивает большую гибкость и лучший контроль над ошибками; он может проверять ошибку и решать, что с ней делать — отображать сообщение пользователю, регистрировать ошибку в файле или базе данных или отправлять по электронной почте, попытаться исправить проблему и продолжить, выйти из выполнения скрипта или вообще игнорировать ошибку.

Функция пользовательского обработчика ошибок должна иметь возможность обрабатывать как минимум два параметра (errno и errstr), однако она может дополнительно принимать три дополнительных параметра (errfile, errline и errcontext), как описано ниже:

Параметр Описание
Обязательно — следующие параметры обязательны
errno. Задает уровень ошибки в виде целого числа. Это соответствует соответствующей константе уровня ошибки (E_ERROR, E_WARNING и т. д.).
errstr. Задает сообщение об ошибке в виде строки.
Опционально — следующие параметры являются необязательными
errfile. Задает имя файла скрипта, в котором произошла ошибка.
errline. Задает номер строки, в которой произошла ошибка.
errcontext. Задает массив, содержащий все переменные и их значения, которые существовали на момент возникновения ошибки. Полезно для отладки.

Вот пример простой пользовательской функции обработки ошибок. Этот обработчик customError() запускается всякий раз, когда возникает ошибка, какой бы тривиальной она ни была. Затем он выводит сведения об ошибке в браузер и останавливает выполнение скрипта.

<?php
// Функция обработчика ошибок
function customError($errno, $errstr){
    echo "<b>Error:</b> [$errno] $errstr";
}
?>

Вам нужно указать PHP, чтобы он использовал вашу пользовательскую функцию обработчика ошибок — просто вызовите встроенную функцию set_error_handler(), передав имя функции.

<?php
// Функция обработчика ошибок
function customError($errno, $errstr){
    echo "<b>Error:</b> [$errno] $errstr";
}
 
// Устанавливаем обработчик ошибок
set_error_handler("customError");
 
// Вызываем ошибку
echo($test);
?>

Регистрация ошибок

Журнал сообщений об ошибках в текстовом файле

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

<?php
function calcDivision($dividend, $divisor){
    if($divisor == 0){
        trigger_error("calcDivision(): The divisor cannot be zero", E_USER_WARNING);
        return false;
    } else{
        return($dividend / $divisor);
    }
}
function customError($errno, $errstr, $errfile, $errline, $errcontext){
    $message = date("Y-m-d H:i:s - ");
    $message .= "Error: [" . $errno ."], " . "$errstr in $errfile on line $errline, ";
    $message .= "Variables:" . print_r($errcontext, true) . "rn";
    
    error_log($message, 3, "logs/app_errors.log");
    die("There was a problem, please try again.");
}
set_error_handler("customError");
echo calcDivision(10, 0);
echo "This will never be printed.";
?>

Отправка сообщений об ошибках по электронной почте

Вы также можете отправить электронное письмо с подробностями об ошибке, используя ту же функцию error_log().

<?php
function calcDivision($dividend, $divisor){
    if ($divisor == 0){
        trigger_error("calcDivision(): The divisor cannot be zero", E_USER_WARNING);
        return false;
    } else{
        return($dividend / $divisor);
    }
}
function customError($errno, $errstr, $errfile, $errline, $errcontext){
    $message = date("Y-m-d H:i:s - ");
    $message .= "Error: [" . $errno ."], " . "$errstr in $errfile on line $errline, ";
    $message .= "Variables:" . print_r($errcontext, true) . "rn";
    
    error_log($message, 1, "webmaster@example.com");
    die("There was a problem, please try again. Error report submitted to webmaster.");
}
set_error_handler("customError");
echo calcDivision(10, 0);
echo "This will never be printed.";
?>

Вызов ошибок

Хотя движок PHP выдает ошибку всякий раз, когда он сталкивается с проблемой в вашем скрипте, вы также можете вызвать ошибки самостоятельно. Это может помочь сделать ваше приложение более надежным, поскольку оно может выявлять потенциальные проблемы до того, как они перерастут в серьезные ошибки.

Чтобы вызвать ошибку в скрипте, вызовите функцию trigger_error(), передав сообщение об ошибке, которое вы хотите сгенерировать:

trigger_error("There was a problem.");

Рассмотрим следующую функцию, которая вычисляет деление двух чисел.

<?php
function calcDivision($dividend, $divisor){
    return($dividend / $divisor);
}
 
// Вызываем функцию
echo calcDivision(10, 0); // Выводит: Warning: Division by zero in C:wampwwwprojecttest.php on line 3
?>

Это сообщение выглядит не очень информативным. Рассмотрим следующий пример, в котором для генерации ошибки используется функция trigger_error().

<?php
function calcDivision($dividend, $divisor){
    if($divisor == 0){
        trigger_error("Делитель не может быть нулевым", E_USER_WARNING);
        return false;
    } else{
        return($dividend / $divisor);
    }
}
 
// Вызываем функцию
echo calcDivision(10, 0); // Выводит: Warning: Делитель не может быть нулевым C:wampwwwprojecterror.php on line 4
?>

Как видите, сообщение об ошибке, созданное во втором примере, более четко объясняет проблему по сравнению с предыдущим.

PHP для начинающих. Обработка ошибок +32

PHP


Рекомендация: подборка платных и бесплатных курсов Java — https://katalog-kursov.ru/

image

Не совершает ошибок только тот, кто ничего не делает, и мы тому пример — сидим и трудимся не покладая рук, читаем Хабр :)

В этой статье я поведу свой рассказа об ошибках в PHP, и о том как их обуздать.

Ошибки

Разновидности в семействе ошибок

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

Чтобы ни одна ошибка не ушла незамеченной потребуется включить отслеживание всех ошибок с помощью функции error_reporting(), а с помощью директивы display_errors включить их отображение:

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

Фатальные ошибки

Самый грозный вид ошибок — фатальные, они могут возникнуть как при компиляции, так и при работе парсера или PHP-скрипта, выполнение скрипта при этом прерывается.

E_PARSE

Это ошибка появляется, когда вы допускаете грубую ошибку синтаксиса и интерпретатор PHP не понимает, что вы от него хотите, например если не закрыли фигурную или круглую скобочку:

<?php
/**
 * Parse error: syntax error, unexpected end of file
 */
{

Или написали на непонятном языке:

<?php
/**
 * Parse error: syntax error, unexpected '...' (T_STRING)
 */
Тут будет ошибка парсера

Лишние скобочки тоже встречаются, и не так важно круглые либо фигурные:

<?php
/**
 * Parse error: syntax error, unexpected '}'
 */
}

Отмечу один важный момент — код файла, в котором вы допустили parse error не будет выполнен, следовательно, если вы попытаетесь включить отображение ошибок в том же файле, где возникла ошибка парсера то это не сработает:

<?php
// этот код не сработает
error_reporting(E_ALL);
ini_set('display_errors', 1);

// т.к. вот тут
ошибка парсера

E_ERROR

Это ошибка появляется, когда PHP понял что вы хотите, но сделать сие не получилось ввиду ряда причин. Эта ошибка так же прерывает выполнение скрипта, при этом код до появления ошибки сработает:

Не был найден подключаемый файл:

/**
 * Fatal error: require_once(): Failed opening required 'not-exists.php' 
 * (include_path='.:/usr/share/php:/usr/share/pear')
 */
require_once 'not-exists.php';

Было брошено исключение (что это за зверь, расскажу немного погодя), но не было обработано:

/**
 * Fatal error: Uncaught exception 'Exception'
 */
throw new Exception();

При попытке вызвать несуществующий метод класса:

/**
 * Fatal error: Call to undefined method stdClass::notExists()
 */
$stdClass = new stdClass();
$stdClass->notExists();

Отсутствия свободной памяти (больше, чем прописано в директиве memory_limit) или ещё чего-нить подобного:

/**
 * Fatal Error: Allowed Memory Size
 */
$arr = array();

while (true) {
    $arr[] = str_pad(' ', 1024);
}

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

Рекурсивный вызов функции. В данном примере он закончился на 256-ой итерации, ибо так прописано в настройках xdebug (да, данная ошибка может проявиться в таком виде только при включении xdebug расширения):

/**
 * Fatal error: Maximum function nesting level of '256' reached, aborting!
 */
function deep() {
    deep();
}
deep();

Не фатальные

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

E_WARNING

Частенько встречается, когда подключаешь файл с использованием include, а его не оказывается на сервере или вы ошиблись указывая путь к файлу:

/**
 * Warning: include_once(): Failed opening 'not-exists.php' for inclusion
 */
include_once 'not-exists.php';

Бывает, если используешь неправильный тип аргументов при вызове функций:

/**
 * Warning: join(): Invalid arguments passed
 */
join('string', 'string');

Их очень много, и перечислять все не имеет смысла…

E_NOTICE

Это самые распространенные ошибки, мало того, есть любители отключать вывод ошибок и клепают их целыми днями. Возникают при целом ряде тривиальных ошибок.

Когда обращаются к неопределенной переменной:

/**
 * Notice: Undefined variable: a
 */
echo $a;

Когда обращаются к несуществующему элементу массива:

/**
 * Notice: Undefined index: a
 */
$b = [];
$b['a'];

Когда обращаются к несуществующей константе:

/**
 * Notice: Use of undefined constant UNKNOWN_CONSTANT - assumed 'UNKNOWN_CONSTANT'
 */
echo UNKNOWN_CONSTANT;

Когда не конвертируют типы данных:

/**
 * Notice: Array to string conversion
 */
echo array();

Для избежания подобных ошибок — будьте внимательней, и если вам IDE подсказывает о чём-то — не игнорируйте её:

PHP E_NOTICE in PHPStorm

E_STRICT

Это ошибки, которые научат вас писать код правильно, чтобы не было стыдно, тем более IDE вам эти ошибки сразу показывает. Вот например, если вызвали не статический метод как статику, то код будет работать, но это как-то неправильно, и возможно появление серьёзных ошибок, если в дальнейшем метод класса будет изменён, и появится обращение к $this:

/**
 * Strict standards: Non-static method Strict::test() should not be called statically
 */
class Strict {
    public function test() {
        echo "Test";
    }
}

Strict::test();

Данный тип ошибок актуален для PHP версии 5.6, и практически все их выпилили из
7-ки. Почитать подробней можно в соответствующей RFC. Если кто знает где ещё остались данные ошибки, то напишите в комментариях

E_DEPRECATED

Так PHP будет ругаться, если вы используете устаревшие функции (т.е. те, что помечены как deprecated, и в следующем мажорном релизе их не будет):

/**
 * Deprecated: Function split() is deprecated
 */
// данная функция, удалена из PHP 7.0
// считается устаревшей с PHP 5.3
split(',', 'a,b');

В моём редакторе подобные функции будут зачёркнуты:

PHP E_DEPRECATED in PHPStorm

Пользовательские

Этот вид, которые «разводит» сам разработчик кода, я уже давно их не встречал, и не рекомендую вам ими злоупотреблять:

  • E_USER_ERROR — критическая ошибка
  • E_USER_WARNING — не критическая ошибка
  • E_USER_NOTICE — сообщения которые не являются ошибками

Отдельно стоит отметить E_USER_DEPRECATED — этот вид всё ещё используется очень часто для того, чтобы напомнить программисту, что метод или функция устарели и пора переписать код без использования оной. Для создания этой и подобных ошибок используется функция trigger_error():

/**
 * @deprecated Deprecated since version 1.2, to be removed in 2.0
 */
function generateToken() {
    trigger_error('Function `generateToken` is deprecated, use class `Token` instead', E_USER_DEPRECATED);
    // ...
    // code ...
    // ...
}

Теперь, когда вы познакомились с большинством видов и типов ошибок, пора озвучить небольшое пояснение по работе директивы display_errors:

  • если display_errors = on, то в случае ошибки браузер получит html c текстом ошибки и кодом 200
  • если же display_errors = off, то для фатальных ошибок код ответа будет 500 и результат не будет возвращён пользователю, для остальных ошибок — код будет работать неправильно, но никому об этом не расскажет

Приручение

Для работы с ошибками в PHP существует 3 функции:

  • set_error_handler() — устанавливает обработчик для ошибок, которые не обрывают работу скрипта (т.е. для не фатальных ошибок)
  • error_get_last() — получает информацию о последней ошибке
  • register_shutdown_function() — регистрирует обработчик который будет запущен при завершении работы скрипта. Данная функция не относится непосредственно к обработчикам ошибок, но зачастую используется именно для этого

Теперь немного подробностей об обработке ошибок с использованием set_error_handler(), в качестве аргументов данная функция принимает имя функции, на которую будет возложена миссия по обработке ошибок и типы ошибок которые будут отслеживаться. Обработчиком ошибок может так же быть методом класса, или анонимной функцией, главное, чтобы он принимал следующий список аргументов:

  • $errno — первый аргумент содержит тип ошибки в виде целого числа
  • $errstr — второй аргумент содержит сообщение об ошибке
  • $errfile — необязательный третий аргумент содержит имя файла, в котором произошла ошибка
  • $errline — необязательный четвертый аргумент содержит номер строки, в которой произошла ошибка
  • $errcontext — необязательный пятый аргумент содержит массив всех переменных, существующих в области видимости, где произошла ошибка

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

<?php
// включаем отображение всех ошибок, кроме E_NOTICE
error_reporting(E_ALL & ~E_NOTICE);
ini_set('display_errors', 1);

// наш обработчик ошибок
function myHandler($level, $message, $file, $line, $context) {
    // в зависимости от типа ошибки формируем заголовок сообщения
    switch ($level) {
        case E_WARNING:
            $type = 'Warning';
            break;
        case E_NOTICE:
            $type = 'Notice';
            break;
        default;
            // это не E_WARNING и не E_NOTICE
            // значит мы прекращаем обработку ошибки
            // далее обработка ложится на сам PHP
            return false;
    }
    // выводим текст ошибки
    echo "<h2>$type: $message</h2>";
    echo "<p><strong>File</strong>: $file:$line</p>";
    echo "<p><strong>Context</strong>: $". join(', $', 
    array_keys($context))."</p>";
    // сообщаем, что мы обработали ошибку, и дальнейшая обработка не требуется
    return true;
}

// регистрируем наш обработчик, он будет срабатывать на для всех типов ошибок
set_error_handler('myHandler', E_ALL);

У вас не получится назначить более одной функции для обработки ошибок, хотя очень бы хотелось регистрировать для каждого типа ошибок свой обработчик, но нет — пишите один обработчик, и всю логику отображения для каждого типа описывайте уже непосредственно в нём

С обработчиком, который написан выше есть одна существенная проблема — он не ловит фатальные ошибки, и при таких ошибках вместо сайта пользователи увидят лишь пустую страницу, либо, что ещё хуже, сообщение об ошибке. Дабы не допустить подобного сценария следует воспользоваться функцией register_shutdown_function() и с её помощью зарегистрировать функцию, которая всегда будет выполняться по окончанию работы скрипта:

function shutdown() {
    echo 'Этот текст будет всегда отображаться';
}
register_shutdown_function('shutdown');

Данная функция будет срабатывать всегда!

Но вернёмся к ошибкам, для отслеживания появления в коде ошибки воспользуемся функцией error_get_last(), с её помощью можно получить информацию о последней выявленной ошибке, а поскольку фатальные ошибки прерывают выполнение кода, то они всегда будут выполнять роль «последних»:

function shutdown() {
    $error = error_get_last();
    if (
        // если в коде была допущена ошибка
        is_array($error) &&
        // и это одна из фатальных ошибок
        in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])
     ) {
        // очищаем буфер вывода (о нём мы ещё поговорим в последующих статьях)
        while (ob_get_level()) {
            ob_end_clean();
        }
        // выводим описание проблемы
        echo "Сервер находится на техническом обслуживании, зайдите позже";
    }
}
register_shutdown_function('shutdown');

Хотел обратить внимание, что данный код хоть ещё и встречается для обработки ошибок, и вы возможно вы даже с ним столкнётесь, но он потерял актуальность начиная с 7-ой версии PHP. Что пришло на замену я расскажу чуть погодя.

Задание

Дополнить обработчик фатальных ошибок выводом исходного кода файла где была допущена ошибка, а так же добавьте подсветку синтаксиса выводимого кода.

О прожорливости

Проведём простой тест, и выясним — сколько драгоценных ресурсов кушает самая тривиальная ошибка:

/**
 * Этот код не вызывает ошибок
 */

// засекаем время выполнения скрипта
$time= microtime(true);

define('AAA', 'AAA');
$arr = [];
for ($i = 0; $i < 10000; $i++) {
    $arr[AAA] = $i;
}

printf('%f seconds <br/>', microtime(true) - $time);

В результате запуска данного скрипта у меня получился вот такой результат:

0.002867 seconds

Теперь добавим ошибку в цикле:

/**
 * Этот код содержит ошибку
 */

// засекаем время выполнения скрипта
$time= microtime(true);

$arr = [];
for ($i = 0; $i < 10000; $i++) {
    $arr[BBB] = $i; // тут используем константанту, которая у нас не объявлена
}

printf('%f seconds <br/>', microtime(true) - $time);

Результат ожидаемо хуже, и на порядок (даже на два порядка!):

0.263645 seconds

Вывод однозначен — ошибки в коде приводят к лишней прожорливости скриптов — так что во время разработки и тестирования приложения включайте отображение всех ошибок!

Тестирование проводил на различных версиях PHP и везде разница в десятки раз, так что пусть это будет ещё одним поводом для исправления всех ошибок в коде

Где собака зарыта

В PHP есть спец символ «@» — оператор подавления ошибок, его используют дабы не писать обработку ошибок, а положится на корректное поведение PHP в случае чего:

<?php
echo @UNKNOWN_CONSTANT;

При этом обработчик ошибок указанный в set_error_handler() всё равно будет вызван, а факт того, что к ошибке было применено подавление можно отследить вызвав функцию error_reporting() внутри обработчика, в этом случае она вернёт 0.

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

Задание

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

Исключения

В эру PHP4 не было исключений (exceptions), всё было намного сложнее, и разработчики боролись с ошибками как могли, это было сражение не на жизнь, а на смерть… Окунуться в эту увлекательную историю противостояния можете в статье Исключительный код. Часть 1. Стоит ли её читать сейчас? Не могу дать однозначный ответ, лишь хочу заметить, что это поможет вам понять эволюцию языка, и раскроет всю прелесть исключений.

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

К примеру, скрипт должен сохранить какие-то данные в кеш файл, если что-то пошло не так (нет доступа на запись, нет места на диске), генерируется исключение соответствующего типа, а в обработчике исключений принимается решение — сохранить в другое место или сообщить пользователю о проблеме.

Исключение — это объект класса Exception либо одного из многих его наследников, содержит текст ошибки, статус, а также может содержать ссылку на другое исключение которое стало первопричиной данного. Модель исключений в PHP схожа с используемыми в других языках программирования. Исключение можно инициировать (как говорят, «бросить») при помощи оператора throw, и можно перехватить («поймать») оператором catch. Код генерирующий исключение, должен быть окружен блоком try, для того чтобы можно было перехватить исключение. Каждый блок try должен иметь как минимум один соответствующий ему блок catch или finally:

try {
    // код который может выбросить исключение
    if (random_int(0, 1)) {
        throw new Exception("One");
    }
    echo "Zero"
} catch (Exception $e) {
    // код который может обработать исключение
    echo $e->getMessage();
}

В каких случаях стоит применять исключения:

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

Для иллюстрации первого сценария возьмём уже озвученный пример функции для записи данных в файл — помешать нам может очень много факторов, а для того, чтобы сообщить выше стоящему коду в чем именно была проблема необходимо создать и выбросить исключение:

$directory = __DIR__ . DIRECTORY_SEPARATOR . 'logs';

// директории может не быть
if (!is_dir($directory)) {
    throw new Exception('Directory `logs` is not exists');
}

// может не быть прав на запись в директорию
if (!is_writable($directory)) {
    throw new Exception('Directory `logs` is not writable');
}

// возможно кто-то уже создал файл, и закрыл к нему доступ
if (!$file = @fopen($directory . DIRECTORY_SEPARATOR . date('Y-m-d') . '.log', 'a+')) {
    throw new Exception('System can't create log file');
}

fputs($file, date("[H:i:s]") . " donen");
fclose($file);

Соответственно ловить данные исключения будем примерно так:

try {
    // код который пишет в файл
    // ...
} catch (Exception $e) {
    // выводим текст ошибки
    echo "Не получилось: ". $e->getMessage();
}

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

// исключения файловой системы
class FileSystemException extends Exception {}

// исключения связанные с директориями
class DirectoryException extends FileSystemException {
    // коды исключений
    const DIRECTORY_NOT_EXISTS =  1;
    const DIRECTORY_NOT_WRITABLE = 2;
}

// исключения связанные с файлами
class FileException extends FileSystemException {}

Теперь, если использовать эти исключения то можно получить следующий код:

try {
    // код который пишет в файл
    if (!is_dir($directory)) {
        throw new DirectoryException('Directory `logs` is not exists', DirectoryException::DIRECTORY_NOT_EXISTS);
    }

    if (!is_writable($directory)) {
        throw new DirectoryException('Directory `logs` is not writable', DirectoryException::DIRECTORY_NOT_WRITABLE);
    }

    if (!$file = @fopen($directory . DIRECTORY_SEPARATOR . date('Y-m-d') . '.log', 'a+')) {
        throw new FileException('System can't open log file');
    }

    fputs($file, date("[H:i:s]") . " donen");
    fclose($file);
} catch (DirectoryException $e) {
    echo "С директорией возникла проблема: ". $e->getMessage();
} catch (FileException $e) {
    echo "С файлом возникла проблема: ". $e->getMessage();
} catch (FileSystemException $e) {
    echo "Ошибка файловой системы: ". $e->getMessage();
} catch (Exception $e) {
    echo "Ошибка сервера: ". $e->getMessage();
}

Важно помнить, что Exception — это прежде всего исключительное событие, иными словами исключение из правил. Не нужно использовать их для обработки очевидных ошибок, к примеру, для валидации введённых пользователем данных (хотя тут не всё так однозначно). При этом обработчик исключений должен быть написан в том месте, где он будет способен его обработать. К примеру, обработчик для исключений вызванных недоступностью файла для записи должен быть в методе, который отвечает за выбор файла или методе его вызывающем, для того что бы он имел возможность выбрать другой файл или другую директорию.

Так, а что будет если не поймать исключение? Вы получите «Fatal Error: Uncaught exception …». Неприятно.

Чтобы избежать подобной ситуации следует использовать функцию set_exception_handler() и установить обработчик для исключений, которые брошены вне блока try-catch и не были обработаны. После вызова такого обработчика выполнение скрипта будет остановлено:

// в качестве обработчика событий
// будем использовать анонимную функцию
set_exception_handler(function($exception) {
    /** @var Exception $exception */
    echo $exception->getMessage(), "<br/>n";
    echo $exception->getFile(), ':', $exception->getLine(), "<br/>n";
    echo $exception->getTraceAsString(), "<br/>n";
});

Ещё расскажу про конструкцию с использованием блока finally — этот блок будет выполнен вне зависимости от того, было выброшено исключение или нет:

try {
    // код который может выбросить исключение
} catch (Exception $e) {
    // код который может обработать исключение
    // если конечно оно появится
} finally {
    // код, который будет выполнен при любом раскладе
}

Для понимания того, что это нам даёт приведу следующий пример использования блока finally:

try {
    // где-то глубоко внутри кода
    // соединение с базой данных
    $handler = mysqli_connect('localhost', 'root', '', 'test');

    try {
        // при работе с БД возникла исключительная ситуация
        // ...
        throw new Exception('DB error');
    } catch (Exception $e) {
        // исключение поймали, обработали на своём уровне
        // и должны его пробросить вверх, для дальнейшей обработки
        throw new Exception('Catch exception', 0, $e);
    } finally {
        // но, соединение с БД необходимо закрыть
        // будем делать это в блоке finally
        mysqli_close($handler);
    }

    // этот код не будет выполнен, если произойдёт исключение в коде выше
    echo "Ok";
} catch (Exception $e) {
    // ловим исключение, и выводим текст
    echo $e->getMessage();
    echo "<br/>";
    // выводим информацию о первоначальном исключении
    echo $e->getPrevious()->getMessage();
}

Т.е. запомните — блок finally будет выполнен даже в том случае, если вы в блоке catch пробрасываете исключение выше (собственно именно так он и задумывался).

Для вводной статьи информации в самый раз, кто жаждет ещё подробностей, то вы их найдёте в статье Исключительный код ;)

Задание

Написать свой обработчик исключений, с выводом текста файла где произошла ошибка, и всё это с подсветкой синтаксиса, так же не забудьте вывести trace в читаемом виде. Для ориентира — посмотрите как это круто выглядит у whoops.

PHP7 — всё не так, как было раньше

Так, вот вы сейчас всю информацию выше усвоили и теперь я буду грузить вас нововведениями в PHP7, т.е. я буду рассказывать о том, с чем вы будете сталкиваться работая над современным PHP проектом. Ранее я вам рассказывал и показывал на примерах какой костыль нужно соорудить, чтобы отлавливать критические ошибки, так вот — в PHP7 это решили исправить, но? как обычно? завязались на обратную совместимость кода, и получили хоть и универсальное решение, но оно далеко от идеала. А теперь по пунктам об изменениях:

  1. при возникновении фатальных ошибок типа E_ERROR или фатальных ошибок с возможностью обработки E_RECOVERABLE_ERROR PHP выбрасывает исключение
  2. эти исключения не наследуют класс Exception (помните я говорил об обратной совместимости, это всё ради неё)
  3. эти исключения наследуют класс Error
  4. оба класса Exception и Error реализуют интерфейс Throwable
  5. вы не можете реализовать интерфейс Throwable в своём коде

Интерфейс Throwable практически полностью повторяет нам Exception:

interface Throwable
{
    public function getMessage(): string;
    public function getCode(): int;
    public function getFile(): string;
    public function getLine(): int;
    public function getTrace(): array;
    public function getTraceAsString(): string;
    public function getPrevious(): Throwable;
    public function __toString(): string;
}

Сложно? Теперь на примерах, возьмём те, что были выше и слегка модернизируем:

try {
    // файл, который вызывает ошибку парсера
    include 'e_parse_include.php';
} catch (Error $e) {
    var_dump($e);
}

В результате ошибку поймаем и выведем:

object(ParseError)#1 (7) {
    ["message":protected] => string(48) "syntax error, unexpected 'будет' (T_STRING)"
    ["string":"Error":private] => string(0) ""
    ["code":protected] => int(0)
    ["file":protected] => string(49) "/www/education/error/e_parse_include.php"
    ["line":protected] => int(4)
    ["trace":"Error":private] => array(0) { }
    ["previous":"Error":private] => NULL
}

Как видите — поймали исключение ParseError, которое является наследником исключения Error, который реализует интерфейс Throwable, в доме который построил Джек. Ещё есть множество других исключений, но не буду мучать — для наглядности приведу иерархию исключений:

interface Throwable
|- Exception implements Throwable
|   |- ErrorException extends Exception
|   |- ... extends Exception
|   `- ... extends Exception
`- Error implements Throwable
    |- TypeError extends Error
    |- ParseError extends Error
    |- ArithmeticError extends Error
    |  `- DivisionByZeroError extends ArithmeticError
    `- AssertionError extends Error

И чуть-чуть деталей:

TypeError — для ошибок, когда тип аргументов функции не совпадает с передаваемым типом:

try {
    (function(int $one, int $two) {
        return;
    })('one', 'two');
} catch (TypeError $e) {
    echo $e->getMessage();
}

ArithmeticError — могут возникнуть при математических операциях, к примеру когда результат вычисления превышает лимит выделенный для целого числа:

try {
    1 << -1;
} catch (ArithmeticError $e) {
    echo $e->getMessage();
}

DivisionByZeroError — ошибка деления на ноль:

try {
    1 / 0;
} catch (ArithmeticError $e) {
    echo $e->getMessage();
}

AssertionError — редкий зверь, появляется когда условие заданное в assert() не выполняется:

ini_set('zend.assertions', 1);
ini_set('assert.exception', 1);

try {
    assert(1 === 0);
} catch (AssertionError $e) {
    echo $e->getMessage();
}

При настройках production-серверов, директивы zend.assertions и assert.exception отключают, и это правильно

Полный список предопределённых исключений вы найдёте в официальном мануале, там же иерархия SPL исключений.

Задание

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

При написании данного раздела были использованы материалы из статьи Throwable Exceptions and Errors in PHP 7.

Единообразие

— Там ошибки, тут исключения, а можно это всё как-то до кучи сгрести?

Да запросто, у нас же есть set_error_handler() и никто нам не запретит внутри оного обработчика бросить исключение:

// Бросаем исключение вместо ошибок
function errorHandler($severity, $message, $file = null, $line = null)
{
    // Кроме случаев, когда мы подавляем ошибки с помощью @
    if (error_reporting() === 0) {
        return false;
    }
    throw new ErrorException($message, 0, $severity, $file, $line);
}

// Будем обрабатывать все-все ошибки
set_error_handler('errorHandler', E_ALL);

Но данный подход с PHP7 избыточен, со всем теперь справляется Throwable:

try {
    /** ... **/
} catch (Throwable $e) {
    // отображение любых ошибок и исключений
    echo $e->getMessage();
}

Отладка

Иногда, для отладки кода, нужно отследить что происходило с переменной или объектом на определённом этапе, для этих целей есть функция debug_backtrace() и debug_print_backtrace() которые вернут историю вызовов функций/методов в обратном порядке:

<?php
function example() {
    echo '<pre>';
    debug_print_backtrace();
    echo '</pre>';
}

class ExampleClass {
    public static function method () {
        example();
    }
}

ExampleClass::method();

В результате выполнения функции debug_print_backtrace() будет выведен список вызовов приведших нас к данной точке:

#0  example() called at [/www/education/error/backtrace.php:10]
#1  ExampleClass::method() called at [/www/education/error/backtrace.php:14]

Проверить код на наличие синтаксических ошибок можно с помощью функции php_check_syntax() или же команды php -l [путь к файлу], но я не встречал использования оных.

Assert

Отдельно хочу рассказать о таком экзотическом звере как assert() в PHP. Собственно, этот кусочек можно рассматривать как мимикрию под контрактную методологию программирования, и дальше я расскажу вам как я никогда его не использовал :)

Функция assert() поменяла своё поведение при переходе от версии 5.6 к 7.0, и ещё сильней всё поменялось в версии 7.2, так что внимательней читайте changelog’и PHP ;)

Первый случай — это когда вам надо написать TODO прямо в коде, да так, чтобы точно не забыть реализовать заданный функционал:

// включаем asserts в php.ini
// zend.assertions=1
assert(false, "Remove it!");

В результате выполнения данного кода получим E_WARNING:

Warning: assert(): Remove it! failed

PHP7 можно переключить в режим exception, и вместо ошибки будет всегда появляться исключение AssertionError:

// переключаем в режим «исключений»
ini_set('assert.exception', 1);

assert(false, "Remove it!");

В результате ожидаемо получаем исключение AssertionError.

При необходимости, можно выбрасывать произвольное исключение:

assert(false, new Exception("Remove it!"));

Я бы рекомендовал использовать метки @TODO, современные IDE отлично с ними работают, и вам не нужно будет прикладывать дополнительные усилия и ресурсы для работы с ними, хотя с ними велик соблазн «забить»

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

// callback-функция для вывода информации в браузер
function backlog($script, $line, $code, $message) {
    echo $message;
}

// устанавливаем callback-функцию
assert_options(ASSERT_CALLBACK, 'backlog');

// отключаем вывод предупреждений
assert_options(ASSERT_WARNING,  false);

// пишем проверку и её описание
assert(sqr(4) === 16, 'When I send integer, function should return square of it');

// функция, которую проверяем
function sqr($a) {
    return; // она не работает
}

Третий вариант — некое подобие на контрактное программирование, когда вы описали правила использования своей библиотеки, но хотите точно убедится, что вас поняли правильно, и в случае чего сразу указать разработчику на ошибку (я вот даже не уверен, что правильно его понимаю, но пример кода вполне рабочий):

/**
 * Настройки соединения должны передаваться в следующем виде
 *
 *     [
 *         'host' => 'localhost',
 *         'port' => 3306,
 *         'name' => 'dbname',
 *         'user' => 'root',
 *         'pass' => ''
 *     ]
 *
 * @param $settings
 */
function setupDb ($settings) {
    // проверяем настройки
    assert(isset($settings['host']), 'Db `host` is required');
    assert(isset($settings['port']) && is_int($settings['port']), 'Db `port` is required, should be integer');
    assert(isset($settings['name']), 'Db `name` is required, should be integer');

    // соединяем с БД
    // ...
}

setupDb(['host' => 'localhost']);

Если вас заинтересовали контракты, то специально для вас у меня есть ссылочка на фреймворк PhpDeal.

Никогда не используйте assert() для проверки входных параметров, ведь фактически assert() интерпретирует первый параметр (ведёт себя как eval()), а это чревато PHP-инъекцией. И да, это правильное поведение, ведь если отключить assert’ы, то все передаваемые аргументы будут проигнорированы, а если делать как в примере выше, то код будет выполняться, а внутрь отключенного assert’a будет передан булевый результат выполнения. А, и это поменяли в PHP 7.2 :)

Если у вас есть живой опыт использования assert() — поделитесь со мной, буду благодарен. И да, вот вам ещё занимательно чтива по этой теме — PHP Assertions, с таким же вопросом в конце :)

В заключение

Я за вас напишу выводы из данной статьи:

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

P.S.

Это репост из серии статей «PHP для начинающих»:

  • Сессия
  • Подключение файлов
  • Обработка ошибок

Если у вас есть замечания по материалу статьи, или возможно по форме, то описывайте в комментариях суть, и мы сделаем данный материал ещё лучше.

Спасибо Максиму Слесаренко за помощь в написании статьи.

За последние 24 часа нас посетили 11535 программистов и 832 робота. Сейчас ищут 403 программиста …

Обработка и протоколирование ошибок

  • Введение
  • Установка и настройка
    • Требования
    • Установка
    • Настройка во время выполнения
    • Типы ресурсов
  • Предопределенные константы
  • Примеры
  • Функции обработки ошибок
    • debug_backtrace — Выводит стек вызовов функций в массив
    • debug_print_backtrace — Выводит стек вызовов функций
    • error_clear_last — Clear the most recent error
    • error_get_last — Получение информации о последней произошедшей ошибке
    • error_log — Отправляет сообщение об ошибке заданному обработчику ошибок
    • error_reporting — Задает, какие ошибки PHP попадут в отчет
    • restore_error_handler — Восстанавливает предыдущий обработчик ошибок
    • restore_exception_handler — Восстанавливает предыдущий обработчик исключений
    • set_error_handler — Задает определенный пользователем обработчик ошибок
    • set_exception_handler — Задает пользовательский обработчик исключений
    • trigger_error — Вызывает пользовательскую ошибку/предупреждение/уведомление
    • user_error — Синоним для trigger_error

Вернуться к: Изменение поведения PHP

Russian (Pусский) translation by Anna Goorikova (you can also view the original English article)

В этом посте вы узнаете, как использовать обработку исключений в PHP. Начиная с PHP 5, мы можем использовать блоки try catch для обработки ошибок — это лучший способ обработки исключений и управления потоком вашего приложения. В этой статье мы рассмотрим основы обработки исключений вместе с несколькими примерами из реального мира.

Что такое исключение?

В PHP 5 появилась новая модель ошибок, которая позволяет вам кидать и ловить исключения в вашем приложении — это лучший способ обработки ошибок, чем то, что мы имели в более старых версиях PHP. Все исключения являются экземплярами базового класса Exception, который мы можем расширить, чтобы ввести наши собственные пользовательские исключения.

Здесь важно отметить, что обработка исключений отличается от обработки ошибок. При обработке ошибок мы можем использовать функцию set_error_handler для установки нашей настраиваемой функции обработки ошибок, чтобы всякий раз, когда срабатывает ошибка, она вызывала нашу функцию обработки ошибок. Таким образом, вы можете управлять ошибками. Однако, как правило, некоторые виды ошибок не восстанавливаются и прекращают выполнение программы.

С другой стороны, исключения — это что-то, что умышленно вызывает код, и ожидается, что он будет пойман в какой-то момент вашего приложения. Таким образом, мы можем сказать, что исключения восстанавливаются, а не определенные ошибки, которые не подлежат восстановлению. Если исключение, которое выбрасывается, попадает где-то в ваше приложение, выполнение программы продолжается с момента, когда исключение было поймано. А исключение, которое не попадает нигде в ваше приложение, приводит к ошибке, которое останавливает выполнение программы.

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

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

Exception handling in a try catch finally blockException handling in a try catch finally blockException handling in a try catch finally block

Исключения могут быть выброшены и пойманы с помощью блоков try и catch. Вы несете ответственность за выброс исключений, если что-то произойдет, чего не ожидается. Давайте быстро рассмотрим основной поток обработки исключений, как показано в следующем псевдокоде.

1
// code before the try-catch block

2

3
try {
4
  // code

5

6
  // if something is not as expected

7
      // throw exception using the "throw" keyword

8

9
  // code, it won't be executed if the above exception is thrown

10
} catch (Exception $e) {
11
  // exception is raised and it'll be handled here

12
  // $e->getMessage() contains the error message

13
}
14

15
// code after the try-catch block, will always be executed

В большинстве случаев, когда вы имеете дело с исключениями, вы в конечном итоге используете шаблон, как показано в приведенном выше фрагменте. Вы также можете использовать блок finally вместе с блоками try и catch, но мы вернемся к этому позже в этой статье.

Блок try — тот, который используется, когда вы подозреваете, что ваш код может генерировать исключение. Вы всегда должны обертывать такой код, используя try и catch.

Выброс исключения

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

Здесь важно отметить, что если вы выбросите исключение, но вы не определили блок catch, который должен обрабатывать это исключение, это приведет к фатальной ошибке. Поэтому вам нужно убедиться, что вы всегда определяете блок catch, если вы бросаете исключения в своем приложении.

Когда исключение попадает в блок catch, объект Exception содержит сообщение об ошибке, которое было выбрано с использованием ключевого слова throw. Переменная $e в приведенном выше примере является экземпляром класса Exception, поэтому она имеет доступ ко всем методам этого класса. В этом блоке вы должны определить свою собственную логику обработки исключений — что именно вы хотите сделать с ошибкой, которую вы поймаете.

В следующем разделе мы рассмотрим пример из реального мира, чтобы понять, как работает обработка исключений.

Пример из реального мира

В этом разделе мы построим реальный пример для демонстрации обработки исключений в PHP.

Предположим, что вы создали приложение, которое загружает конфигурацию приложения из файла config.php. Теперь важно, чтобы файл config.php присутствовал, когда ваше приложение загружается. Таким образом, ваше приложение не может работать, если файл config.php отсутствует. Так что это идеальный случай, чтобы выбросить исключение и сообщить пользователю, что им необходимо исправить проблему.

1
<?php
2
try {
3
    // init bootstrapping phase

4

5
    $config_file_path = "config.php";
6

7
    if (!file_exists($config_file_path))
8
    {
9
      throw new Exception("Configuration file not found.");
10
    }
11
 
12
    // continue execution of the bootstrapping phase

13
} catch (Exception $e) {
14
    echo $e->getMessage();
15
    die();
16
}
17
?>

Как вы можете видеть в приведенном выше примере, мы проверяем наличие файла config.php в начале фазы начальной загрузки. Если файл config.php найден, выполнение продолжается в обычном режиме. С другой стороны, мы выбросим исключение, если файл config.php не существует. Кроме того, мы хотели бы прекратить выполнение, если есть исключение!

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

Таким образом, это был пример обработки исключений с использованием класса Exception по умолчанию. В следующем разделе мы рассмотрим, как вы можете расширить основной класс Exception и создать свои собственные пользовательские исключения в своем приложении.

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

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

В предыдущем примере мы выбрали исключение конфигурации, используя класс Exception по умолчанию. Это прекрасно, если вы просто хотите иметь дело с сообщением об ошибке. Однако иногда вы хотите сделать немного больше в зависимости от типа исключения, которое бросается. Вот почему пользовательские исключения полезны.

Перейдем к предыдущему примеру, как показано в следующем фрагменте.

1
<?php
2
class ConfigFileNotFoundException extends Exception {}
3

4
try {
5
    // init bootstrapping phase

6

7
    $config_file_path = "config.php";
8

9
    if (!file_exists($config_file_path))
10
    {
11
      throw new ConfigFileNotFoundException("Configuration file not found.");
12
    }
13
 
14
    // continue execution of the bootstrapping phase

15
} catch (ConfigFileNotFoundException $e) {
16
    echo "ConfigFileNotFoundException: ".$e->getMessage();
17
    // other additional actions that you want to carry out for this exception

18
    die();
19
} catch (Exception $e) {
20
    echo $e->getMessage();
21
    die();
22
}
23
?>

Во-первых, мы определили класс ConfigFileNotFoundException, который расширяет основной класс Exception. Теперь он становится нашим настраиваемым классом исключений, и мы можем использовать его, когда хотим выбросить исключение ConfigFileNotFoundException в нашем приложении.

Затем мы использовали ключевое слово throw для исключения исключений ConfigFileNotFoundException в случае, если файл config.php не существует. Однако важное различие находится в блоке catch. Как вы можете видеть, мы определили два блока catch, и каждый блок используется для обнаружения различного типа исключения.

Первый получает исключения типа ConfigFileNotFoundException. Итак, если генерируемое исключение относится к типу ConfigFileNotFoundException, этот блок будет выполнен. Если тип исключения не соответствует какому-либо конкретному блоку catch, он будет соответствовать последнему, который должен поймать все генерические сообщения об исключениях.

Блок Finally

В этом разделе мы рассмотрим, как вы можете использовать ключевое слово finally вместе с блоками try и catch. Иногда вы хотите выполнить часть кода независимо от того, было ли исключено исключение. Вот где вы можете использовать блок finally, поскольку код, который вы размещаете в блоке finally, всегда будет выполняться после выполнения блоков try и catch независимо от того, было ли выбрано исключение.

Попробуем понять это, используя следующий пример.

1
try {
2
  // code

3

4
  // if something is not as expected

5
      // throw exception using the "throw" keyword

6

7
  // code, it won't be executed if the above exception is thrown

8
} catch (Exception $e) {
9
  // exception is raised and it'll be handled here

10
  // $e->getMessage() contains the error message

11
} finally {
12
  // code, it'll always be executed

13
}

Код в приведенном выше примере почти то же самое с единственным исключением, что мы добавили блок finally после блока catch. И, как мы обсуждали, код в этом блоке всегда будет выполняться.

Типичные прецеденты, которые мы могли бы придумать для использования блока finally, обычно связаны с очисткой ресурсов. Например, если вы открыли соединение с базой данных или файл на диске в блоке try, вы можете выполнять задачи очистки, такие как закрытие соединения в блоке finally, так как это гарантировано.

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

Заключение

Сегодня мы обсудили тему обработки исключений в PHP. В первой половине статьи мы обсудили основы исключений в PHP и создали реальный пример, чтобы продемонстрировать, как они работают. В конце мы рассмотрели, как вы можете создавать пользовательские исключения, расширяя основной класс Exception.

PHP5 Обработка ошибок



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


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

Учебник содержит несколько из наиболее распространенных методов проверки ошибок в PHP.

Вы узнаете различные методы обработки ошибок:

  • Простое заявление это()
  • Пользовательские ошибки и триггеры ошибок
  • Отчеты об ошибках

В первом примере показан простой скрипт, открывающий текстовый файл: использование функции это()

Пример

<?php
$file=fopen(«welcome.txt»,»r»);
?>

Если файл не существует, Вы можете получить ошибку, как эта:

Внимание: fopen(welcome.txt) [function.fopen]: не удалось открыть поток:
Нет такого файла или каталога в C:webfoldertest.php на линии 2

Чтобы запретить пользователю получать сообщение об ошибке, подобное приведенному примеру выше, мы проверяем
файл, существует ли он до того, как мы попытаемся получить к нему доступ:

Пример

<?php
if(!file_exists(«welcome.txt»)) {
  die(«Файл не найден»);
}
else {
  $file=fopen(«welcome.txt»,»r»);
}
?>

Теперь, если файл не существует вы получите ошибку, как эта:

Файл не найден

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

Тем не менее, остановить просто сценарий не всегда правильный путь. Рассмотрим альтернативные функции PHP для обработки ошибок.


PHP Создание пользовательского обработчика ошибок

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

Эта функция должна быть способна обрабатывать, как минимум два параметра (уровень ошибки и сообщение об ошибке),
но можно принимать до пяти параметров (дополнительно: файл, номер строки и контекст ошибки):

Синтаксис

error_function(error_level,error_message,
error_file,error_line,error_context)

Параметр Описание
error_level Необходимо. Указывает уровень отчета об ошибках для пользовательской ошибки.
Должно быть числовое значение. См. таблицу ниже для возможных уровней отчета об ошибках
error_message Необходимо. Указывает сообщение об ошибке определяемая пользователем
error_file Необязательно. Задает имя файла, в котором произошла ошибка
error_line Необязательно. Указывает номер строки, в которой произошла ошибка
error_context Необязательно. Задает массив, содержащий все переменные и их значения, используемые при возникновении ошибки

PHP Уровни отчетов об ошибках

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

Значение Констант Описание
2 E_WARNING Неустранимые ошибки выполнения. Выполнение скрипта не останавливается
8 E_NOTICE Уведомления среды выполнения. Сценарий нашел что-то, что могло бы быть ошибкой, но могло бы также произойти при запуске сценария, как обычно
256 E_USER_ERROR Неустранимая ошибка пользователя. Это похоже на набор E_ERROR установленный программистом с помощью функции PHP trigger_error()
512 E_USER_WARNING Неустранимое пользовательское предупреждение. Это похоже на набор E_WARNING установленный программистом с помощью функции PHP trigger_error()
1024 E_USER_NOTICE Автоматическое уведомление пользователя. Это похоже на набор E_NOTICE устанавливается программистом с помощью функции PHP trigger_error()
4096 E_RECOVERABLE_ERROR Перехватываемая неустранимая ошибка. Это похоже на набор E_ERROR но может быть перехватана пользователем, определенной обработкой (смотреть также set_error_handler())
8191 E_ALL Все ошибки и предупреждение (E_STRICT становится частью E_ALL в PHP 5.4)

Теперь давайте создадим функцию для обработки ошибок:

Пример

function customError($errno, $errstr) {
  echo «<b>Ошибка:</b> [$errno] $errstr<br>»;
  echo «Конечный Script»;
  die();
}

Приведенный выше код, является простой функцией обработки ошибок. Когда он срабатывает, он получает код ошибки и сообщение об ошибке.
Затем выводится уровень ошибки и сообщение и завершается сценарий.

Теперь, когда Вы создали функцию обработки ошибок, Вы должны решить, когда она должно сработать.


PHP Установить обработчик ошибок

Обработчик ошибок по умолчанию для PHP является встроенным обработчиком ошибок.
Мы собираемся сделать функцию над обработчиком ошибок по умолчанию на время скрипта.

Можно изменить обработчик ошибок для применения только к некоторым ошибкам, таким образом,
сценарий может обрабатывать различные ошибки по-разному.
Однако, в этом примере мы будем использовать наш пользовательский обработчик ошибок для всех ошибок:

set_error_handler(«customError»);

Поскольку мы хотим, чтобы наша пользовательская функция обрабатывала все ошибки, set_error_handler()
требуется только один параметр, второй параметр может быть добавлен, чтобы указать уровень ошибки.

Тестирование обработчика ошибок при попытке вывести несуществующую переменную:

Пример

<?php
//функция обработчика ошибок
function customError($errno, $errstr) {
  echo «<b>Ошибка:</b> [$errno] $errstr»;
}

//установить обработчик ошибок
set_error_handler(«customError»);

//Вызов ошибки
echo($test);
?>

Выходные данные приведенного выше кода должны быть примерно такими:

Ошибка: [8] Неопределенна переменная: test


PHP Вызвать ошибку

В скрипте, где пользователи могут вводить данные, полезно инициировать ошибки, когда происходит незаконный ввод.
В PHP это делается с помощью функции trigger_error().

В этом примере возникает ошибка, если $test переменная больше, чем 1:

Пример

<?php
$test=2;
if ($test>=1)
{

  trigger_error(«Значение должно быть 1 или ниже»);
}
?>

Выходные данные приведенного выше кода должны быть примерно такими:

Заметьте: Значение должно быть 1 или ниже
в C:webfoldertest.php на линии 6

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

Возможные типы ошибок:

  • E_USER_ERROR — Неустранимая пользовательская ошибка выполнения. Ошибки, из которых невозможно восстановить. Выполнение скрипта прекращается
  • E_USER_WARNING — Непоправимое пользовательское предупреждение во время выполнения. Выполнение скрипта не останавливается
  • E_USER_NOTICE — Невыполнение. Уведомление о времени выполнения, созданное пользователем. Сценарий нашел что-то, что могло бы быть ошибкой, но могло бы также произойти при запуске сценария

В этом примере E_USER_WARNING происходит, если переменная $test больше, чем 1. Если происходит E_USER_WARNING мы будем использовать наш пользовательский обработчик ошибок и закончить сценарий:

Пример

<?php
//функция обработчика ошибок
function customError($errno, $errstr) {
  echo «<b>Ошибка:</b> [$errno] $errstr<br>»;
  echo «Закончить Script»;
  die();
}

//установить обработчик ошибок
set_error_handler(«customError»,E_USER_WARNING);

//вызов ошибки
$test=2;
if ($test>=1) {
  trigger_error(«Значение должно быть 1 или ниже»,E_USER_WARNING);
}
?>

Выходные данные приведенного выше кода должны быть примерно такими:

Ошибка: [512] Значение должно быть 1 или ниже
Конец скрипта

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


PHP Регистрация ошибок

По умолчанию, PHP отправляет отчет об ошибке в систему регистрации на сервер или файл,
в зависимости от того, как конфигурация error_log установлена в php.ini-файл. По
с помощью функции error_log() можно отправлять журнал ошибок в указанный файл или в удаленное место назначения.

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

PHP Отправка сообщение об ошибке по электронной почте

В приведенном ниже примере мы отправим электронное письмо с сообщением об ошибке и
сценарий, если возникает ошибка:

Пример

<?php
//функция обработчика ошибок
function customError($errno, $errstr) {
  echo «<b>Ошибка:</b> [$errno] $errstr<br>»;
  echo «Веб-мастер был уведомлен»;
  error_log(«Ошибка: [$errno] $errstr»,1,
  «someone@example.com»,»От: webmaster@example.com»);
}

//установить обработчик ошибок
set_error_handler(«customError»,E_USER_WARNING);

//вызов ошибки
$test=2;
if ($test>=1) {
  trigger_error(«Значение должно быть 1 или ниже»,E_USER_WARNING);
}
?>

Выходные данные приведенного выше кода должны быть примерно такими:

Ошибка: [512] Значение должно быть 1 или ниже
Веб-мастер был уведомлен

И почта, полученная из кода выше, выглядит так:

Ошибка: [512] начение должно быть 1 или ниже

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


Обработка исключений

Курс PHP для начинающих

  • PHP и MySQL
  • Основы PHP
  • Обработка исключений

Внимание! Данный курс устарел!
Переходите к новому курсу «PHP для начинающих».

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

Курс PHP для начинающих

Программисты, знакомые с такими языками структурного программирования, как C# и Java, по-видимому, давно привыкли использовать различные встроенные объекты, позволяющие справляться с ошибками и исключительными ситуациями. Таким специалистам будет приятно узнать, что теперь в версии PHP 5 впервые появился объект, предназначенный для обработки исключительных ситуаций, и что синтаксические конструкции для работы с этим объектом весьма напоминают существующие языки, такие как Java. В действительности даже после краткого знакомства с этими синтаксическими конструкциями разработчик может приступить к обработке ошибок и исключительных ситуаций в языке PHP во многом по такому же принципу, который применяется в других объектно-ориентированных языках.

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

Ошибки и исключительные ситуации

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

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

Ниже приведен пример кода, который содержит некоторые средства обнаружения ошибок в том виде, в каком они могло быть реализованы в версии PHP 4 или в одной из предыдущих версий. В этом коде осуществляется выборка переменной POST, содержащей идентификатор пользователя. Такой идентификатор должен иметь длину по меньшей мере девять символов и начинаться с префикса «usr»:

Пример метода обработки ошибок без использования исключений

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Основы PHP</title>
</head>

<body>

<?php

// Выполнить выборку идентификатора пользователя, подлежащего проверке
$user_id = isset($_POST['user_id']) ? $_POST['user_id'] : '';

// Подготовить для вывода на дисплей сообщение, содержимое которого
// зависит от того, является ли действительным идентификатор пользователя
if (!empty($user_id))
	echo !is_valid_user($user_id) ? '<b style="color:red;">Некорректный логин</b><br>' : "Все правильно<br>";


function is_valid_user($user_id)
{	
	// Возвратить значение false, если идентификатор пользователя 
	// не начинается с подстроки "usr"
	$pre_str = "usr";
	if ((strpos($user_id, $pre_str) === false) || (strpos ($user_id, $pre_str) != 0))
		return false;
		
	// Возвратить значение false, если идентификатор пользователя 
	// не соответствует требованиям по длине 
	if ((strlen($user_id) < 9)) return false;
	
	return true;
}

?>

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
    <input type="text" name="user_id" placeholder="Введите логин"><br>
    <input type="submit" value="Отправить">
</form>

</body>
</html>

Вполне очевидно, что функция is_valid_user() может вернуть значение false вследствие несоблюдения любого из указанных требований. Применение исключений позволяет проще организовать выявление ошибок различных типов, после чего принимать соответствующие меры, учитывая характер каждой отдельной ошибки.

Класс Exception

В версии PHP 5 предусмотрен новый класс Exception, который может непосредственно использоваться в любом коде, выполняемом под управлением сервера PHP 5 или более поздней версии. В примере, приведенном выше, применялись булевы функции, а класс Exception позволяет создавать или активизировать в коде исключительные ситуации — экземпляры этого класса.

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

Код PHP

<?php

// Выполнить выборку идентификатора пользователя, подлежащего проверке
$user_id = isset($_POST['user_id']) ? $_POST['user_id'] : '';

// Подготовить для вывода на дисплей сообщение, содержимое которого
// зависит от того, является ли действительным идентификатор пользователя
try
{
	if (!empty($user_id) && is_valid_user($user_id))
		echo "Все правильно<br>";
}
catch (Exception $ex)
{
	$msg = $ex->getMessage();
	echo $msg;
}


function is_valid_user($user_id)
{	
	// Активизировать исключение, если идентификатор пользователя 
	// не начинается с подстроки "usr"
	$pre_str = "usr";
	if ((strpos($user_id, $pre_str) === false) || (strpos ($user_id, $pre_str) != 0))
		throw new Exception('<b style="color:red;">Некорректный логин - должен начинаться со строки "usr"</b><br>');
		
	// Активизировать исключение, если идентификатор пользователя 
	// не соответствует требованиям по длине 
	if ((strlen($user_id) < 9)) 
		throw new Exception('<b style="color:red;">Некорректный логин - длина меньше 9 символов</b><br>');
	
	return true;
}

?>

Использование исключений

В этом коде для активизации исключения применяется оператор throw. А сами средства обработки исключительных ситуаций в данном случае использовались для создания нового объекта Exception. Благодаря применению этих средств появляется возможность обрабатывать ошибки, нарушающие нормальный ход выполнения программы, как отдельные исключительные ситуации, поэтому ошибки больше не могут влиять на остальную часть приложения.

Перехват и обработка исключительных ситуаций осуществляется с помощью управляющей конструкции try/catch. Прежде всего необходимо включить любой код, выполнение которого может привести к возникновению ошибки или исключительной ситуации, в конструкцию try(). При активизации в этом коде любой исключительной ситуации выполнение блока try() прекращается; это означает, что оставшийся код в конструкции try() не выполняется. Затем блок catch() просматривается для поиска исключительной ситуации соответствующего типа, а обработка исключительной ситуации происходит с помощью кода, содержащегося в данном конкретном блоке catch. Благодаря такой организации работы может быть предусмотрен анализ различных условий с учетом типа активизированной исключительной ситуации, но не предпринимается безнадежная попытка справиться с какой-то общей, неконкретизированной ошибкой.

Активизация исключительной ситуации

Благодаря использованию исключительных ситуациях появляется еще одна удобная возможность — предоставлять пользователю настолько много (или настолько мало) информации об ошибке, насколько это целесообразно. В сочетании с объектом Exception допускается применение нескольких методов обработки исключительных ситуаций, которые могут использоваться для создания собственных сообщений об ошибках или организации правильных действий в сложившейся ситуации.

Ниже приведен код, который показывает, как можно активизировать и сразу же перехватить универсальную исключительную ситуацию, а затем применить методы объекта Exception для извлечения переданного сообщения, кода ошибки, имени файла и номера строки, ставшей источником ошибки:

Код PHP

try
{
	if (!empty($user_id) && is_valid_user($user_id))
		echo "Все правильно<br>";
}
catch (Exception $ex)
{
	// Входная строка, переданная объекту 
	$msg = $ex->getMessage(); 
	
	// Определяемый пользователем код ошибки 
	$code = $ex->getCode();
	
	// Имя файла, при обработке которого была активизирована 
	// исключительная ситуация 
	$file = $ex->getFile();
	
	// Номер строки, при обработке которой возникла исключительная ситуация 
	$line = $ex->getLine();
	
	echo "Ошибка в коде $code: $msg Файл: $file, номер строки: $line<br><br>";
}

Получение дополнительной информации об исключении

В данном случае отображается стандартное сообщение об ошибке в стиле PHP. Однако программист может предусмотреть вывод любой информации, какую он сочтет необходимой, и даже может изменить действия, выполняемые в приложении, с учетом конкретной ошибки. Для целенаправленной обработки исключительных ситуаций больше чем одного типа могут использоваться многочисленные блоки catch. Пример такой организации программы приведен в следующем разделе.

Определение собственных подклассов Exception

Язык PHP позволяет также определять собственные классы, которые наследуют методы класса Exception. Это означает, что можно больше не ограничиваться использованием лишь функции getMessage() для получения информации о том, какой именно тип имеет возникшая ошибка. Подклассы могут быть определены так, как показано в следующем примере:

Код PHP

class CustomException extends Exception {
	public function __construct($message) {
		parent::__construct($message);
	}
}

Рассмотрим пример, приведенный ниже, который демонстрирует способы использования специализированных исключительных ситуаций. Этот код предназначен для обеспечения взаимодействия с пользователями, подписывающимися на определенные услуги. Пользователи могут забыть включить обязательный префикс «usr» в свой идентификатор пользователя поэтому при отсутствии этого префикса можно предусмотреть возможность добавить его и снова проверить подлинность пользователя, а не прекращать выполнение программы:

Код PHP

<?php

// Определить специализированные классы исключений
class PrefixException extends Exception {
	public function __construct($message) {
		parent::__construct($message);
	}
}

class MaxLengthException extends Exception {
	public function __construct($message) {
		parent::__construct($message);
	}
}

// Выполнить выборку идентификатора пользователя, подлежащего проверке
$user_id = isset($_POST['user_id']) ? $_POST['user_id'] : '';

// Подготовить для вывода на дисплей сообщение, содержимое которого
// зависит от того, является ли действительным идентификатор пользователя
try
{
	if (!empty($user_id) && is_valid_user($user_id))
		echo "Все правильно<br>";
}
catch (PrefixException $ex)
{
	// Если префикс "usr" отсутствует, добавить его
	$user_id = "usr".$user_id;
	
	// Повторный вызов проверки
	try {
		if (is_valid_user($user_id))
			echo "Все правильно<br>";
	} catch (Exception $ex) { 
		echo $ex->getMessage();
	}
}
catch (MaxLengthException $ex)
{
	$msg = $ex->getMessage(); 
	echo $msg;
}


function is_valid_user($user_id)
{	
	// Активизировать исключение, если идентификатор пользователя 
	// не начинается с подстроки "usr"
	$pre_str = "usr";
	if ((strpos($user_id, $pre_str) === false) || (strpos ($user_id, $pre_str) != 0))
		throw new PrefixException('<b style="color:red;">Некорректный логин - должен начинаться со строки "usr"</b><br>');
		
	// Активизировать исключение, если идентификатор пользователя 
	// не соответствует требованиям по длине 
	if ((strlen($user_id) < 9)) 
		throw new MaxLengthException('<b style="color:red;">Некорректный логин - длина меньше 9 символов</b><br>');
	
	return true;
}

?>

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

Ограничения средств обработки исключений языка PHP

Объект Exception — новое средство, появившееся в версии PHP 5, поэтому этот объект, как таковой, все еще находится на самых ранних этапах разработки. Ко времени написания этих строк язык PHP не поддерживал использование таких конструкций, как finally() и throws(), предусмотренных в языке Java и других языках. Кроме того, в отличие от других языков на исключительные ситуации еще не отображаются ошибки, распознаваемые самим интерпретатором PHP (в том числе ошибки, сообщения о которых обычно появляются в клиентском браузере). Из-за этого, например, возникновение ошибки в операторе SQL, выполняемом в блоке try/catch, не приводит автоматически к активизации исключительной ситуации, которую можно было бы перехватить и заняться ее обработкой. Такие удобные функциональные средства, по всей вероятности, должны быть включены в одну из будущих версий PHP, поэтому для авторов имеет смысл упомянуть о них, а для читателей — следить за их появлением.

Cookie-файлы и сеансы

Нарушения в работе системы PHP

Оценить статью:

При работе над веб-приложениями программисту легко попасть в ловушку: разрабатывать и тестировать только понятные сценарии, в которых всё происходит правильно. К сожалению, в реальности встречаются ситуации, в которых всё идёт не так, как планировалось. Обработка ошибок — важная часть пользовательского опыта любого приложения. Если приложение реагирует на ошибки правильно, ваши пользователи будут знать, что делать дальше, даже если что-то идёт не так.

  • Обработка ошибок в бэкенд- и фронтенд-приложениях: в чём разница
  • Как правильно обрабатывать ошибки
  • Как работать с ошибками в AJAX-запросах и что нужно знать о кодах ответа HTTP
  • Как перехватывать ошибки во фронтенд-приложениях
  • Главное об обработке ошибок во фронтенд-приложениях

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

Ошибки ввода

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

Ошибки авторизации

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

Ошибки доступности

Эти ошибки возникают, когда ресурс, нужный для завершения действия пользователя, по каким-то причинам недоступен. Такие ошибки бывают запланированными (плановое обновление сайта) и незапланированными (выход из строя сервера).

Неожиданные ошибки

Это ошибки, которые обычно говорят о багах в приложении, например, о необработанных исключениях.

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

Примечание — тема обработки ошибок во фронтенд-приложениях подробно рассматривается в рамках профессии «Фронтенд-программист». Базовые курсы в этой профессии, включая «Введение в программирование», «Основы командной строки», «Настройка окружения», «Системы контроля версий», доступны бесплатно после регистрации.

Обработка ошибок в бэкенд- и фронтенд-приложениях: в чём разница

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

Плохо настроенные приложения могут даже показывать конечному пользователю информацию о внутренних ошибках. В большинстве случаев бэкенд-приложения не очень хорошо помогают человеку справиться с ошибкой и вернуться к нормальному использованию приложения. Но они хорошо справляются с задачей информирования пользователя об ошибках.

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

  • Приложение работает, но не выполняет действий, которые ожидает пользователь. Самая распространённая реакция пользователей в такой ситуации — попробовать ещё раз в надежде, что в этот раз приложение поведёт себя ожидаемо.
  • Приложение останавливается, но не сообщает об остановке пользователю. Здесь пользователь повторит действие или попробует выполнить новое действие, но у него ничего не получится.
  • Если ошибка происходит достаточно рано, пользователь может увидеть белый экран из-за неудачной попытки приложения отобразить страницу.

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

Читайте полезную статью
Что такое магические числа в программировании и как снять это заклятие.

Как правильно обрабатывать ошибки

Есть много способов обработки ошибок в JavaScript-приложениях. Вы можете определить глобальный обработчик ошибок, который будет отображать переданные в него сообщения. Также вы можете построить приложение так, чтобы каждый его компонент самостоятельно обрабатывал ошибки, которые в нём возникают.

Один из простых способов обработки ошибок заключается в том, чтобы создать общую схему для реакции на все ошибки и использовать систему событий браузеров, чтобы перехватывать всплывающие ошибки и обрабатывать их. Например, ошибку валидации формы можно перехватить на элементе form или соответствующем инпуте и показать пользователю сообщение об этой ошибке. А нераспознанная системная ошибка может всплыть на уровень document. В этом случае пользователь увидит обобщённое сообщение об ошибке.

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

  • Измените что-то и повторите действие. Если пользователь ввёл невалидные данные и не смог отправить форму, благодаря сообщению об ошибке он сможет исправить данные и отправить форму.
  • Попробуйте позже. Пользователь не смог отправить форму из-за ошибки сети. Благодаря сообщению он вернётся через 10 минут и успешно отправит форму.
  • Свяжитесь с нами. Пользователь не смог отправить форму из-за неожиданной ошибки. Благодаря сообщению об ошибке он свяжется со службой поддержки и решит свои задачи.

При обработке ошибок на стороне клиента часто возникает необходимость выбрать между остановкой и продолжением работы приложения. Если ошибка влияет только на часть системы, можно разрешить человеку пользоваться приложением дальше. Если ошибка критическая или она влияет на разные части приложения, можно показать сообщение в модальном окне, которое невозможно закрыть. Также можно заменить контент страницы сообщением об ошибке. Это защитит пользователя от бесполезных попыток выполнить желаемое действие.

Как работать с ошибками в AJAX-запросах и что нужно знать о кодах ответа HTTP

Самый простой и очень эффективный способ сообщить пользователю об ошибке — правильно использовать коды ответов HTTP. Коды статуса HTTP могут самостоятельно дать пользователю достаточно информации о том, почему возникла ошибка запроса, а также подсказать, что делать дальше.

«Ошибочные» коды ответов HTTP объединяются в две группы: ответы 4XX и ответы 5XX. Первые говорят о проблеме с запросом (клиентские ошибки), а вторые — о проблеме с сервером (серверные ошибки). Ниже перечислены самые распространённые «ошибочные» коды статусов HTTP, которые можно получить при работе с веб-приложением:

  • 400 — Bad Request. Обычно этот статус связан с ошибкой ввода, например, если пользователь вводит некорректный адрес электронной почты.
  • 401 — Unauthorized. Этот статус связан с ситуацией, когда пользователь пытается получить доступ к чему-либо без авторизации там, где авторизация требуется. Также этот код ошибки подходит в ситуации, когда пользователь пытается выполнить действие, на которое у него нет прав.
  • 403 — Forbidden. Разница между этим статусом и статусом 400 незначительная. Обычно код 403 говорит о том, что сервер понял запрос, но не может его выполнить. Например, такой статус можно возвращать, если пользователь ввёл номер акционного купона с истекшим сроком действия.
  • 404 — Not Found. Это самый известный из «ошибочных» кодов ответа. Он сообщает, что запрошенный ресурс не найден. Это может произойти из-за некорректного URL, удалённой или перемещённой страницы.
  • 409 — Conflict. В большинстве случаев этот статус говорит о конфликте управления версиями. Например, такое происходит, если пользователь пробует загрузить версию файла, которая старше загруженной ранее версии этого файла. Также этот код может говорить об ограничениях уникальности, например, если пользователь пытается повторно отправить электронное письмо (второй раз нажимает кнопку «Отправить», не дождавшись завершения действия).
  • 500 — Internal Server Error. Этот статус говорит об ошибке, которую можно описать так: «Что-то пошло не так, но мы не знаем, что именно».
  • 503 — Unavailable. Сервер вышел из строя, ошибка может быть запланированной или незапланированной.

Если вы хорошо знаете эти коды, вам будет проще обрабатывать ошибки, которые возникают при работе с AJAX-запросами.

Примечание — Обратите внимание на сервис httpstat.us, он пригодится вам для тестирования реакций на ошибки при разработке фронтенд-приложений.

Как перехватывать ошибки во фронтенд-приложениях

Вы можете определить обработчик глобально с помощью функции window.onerror. В этом случае обработчик переопределит дефолтное поведение браузеров, благодаря чему ваше приложение будет показывать пользователям полезную информацию при возникновении ошибок.

window.onerror = (message, url, lineNumber) => {
  // определяем, знаем ли мы, как обрабатывать ошибку
  if (errorCanBeHandled) {
    // показываем сообщение об ошибке пользователю
    displayErrorMessage(message);
    // возвращаем true и запускаем дефолтную
    // реакцию приложения на фатальные ошибки
    return true;
  } else {
    // запускаем дефолтную обработку ошибок браузером
    return false;
  }
}

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

Изучайте фронтенд-разработку на Хекслете! Первые курсы в профессии «Фронтенд-программист» доступны бесплатно. Регистрируйтесь и стартуйте в удобное время.

Главное об обработке ошибок во фронтенд-приложениях

Главный факт об обработке ошибок заключается в том, что вы должны их обрабатывать. Любая попытка сообщить пользователю что-то полезное, когда возникает ошибка — отличный ход. Даже информирование с помощью alert() лучше, чем отсутствие информации. Помните, что при проектировании UI вашего приложения нужно учитывать все возможные ситуации, включая различные ошибки.

Адаптированный перевод статьи Front-End Error Handling by Static Apps. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.

Хотя PHP уже давно поддерживает обработку исключений, однако, по сравнению с Java эта поддержка была довольно слабой

Первоначальная поддержка обработки исключений была введена в язык с 5 версии PHP, с двумя простыми встроенными классами исключений — Exception и ErrorException, с поддержкой дополнительных классов через SPL. Идея этого поста состоит в том, чтобы представить читателям современные возможности обработки исключений PHP. 

Новый интерфейс

Хотя PHP 7 предоставляет классы Error и Exception, давайте сначала затронем интерфейс Throwable . И Error и Exception классы реализуют Throwable интерфейс — это основа для любого объекта , который может быть брошен с помощью оператора throw. Единственное, что он не может быть реализован непосредственно в классах пользовательского пространства, только через расширение класса Exception. Кроме того, он обеспечивает единую точку для отлова обоих типов ошибок в одном выражении:

<?php

try {
// ваш код
} catch (Throwable $e) {
echo 'Очень хороший способ отловить исключения и ошибки';
}

Список доступных встроенных классов исключений начиная с PHP 7.4:

  • Exception
  • ErrorException
  • Error
  • ArgumentCountError
  • ArithmeticError
  • AssertionError
  • DivisionByZeroError
  • CompileError
  • ParseError
  • TypeError

Дополнительные классы исключений можно найти в стандартной библиотеке PHP . И наиболее заметным из расширений JSON является класс JsonException.

THROWABLE

Интерфейс Throwable  PHP 7:

interface Throwable
{
public function getMessage(): string; // Error reason
public function getCode(): int; // Error code
public function getFile(): string; // Error begin file
public function getLine(): int; // Error begin line
public function getTrace(): array; // Return stack trace as array like debug_backtrace()
public function getTraceAsString(): string; // Return stack trace as string
public function getPrevious(): Throwable; // Return previous `Trowable`
public function __toString(): string; // Convert into string
}

Вот иерархия Throwable:

interface Throwable
|- Error implements Throwable
|- ArithmeticError extends Error
|- DivisionByZeroError extends ArithmeticError
|- AssertionError extends Error
|- ParseError extends Error
|- TypeError extends Error
|- ArgumentCountError extends TypeError
|- Exception implements Throwable
|- ClosedGeneratorException extends Exception
|- DOMException extends Exception
|- ErrorException extends Exception
|- IntlException extends Exception
|- LogicException extends Exception
|- BadFunctionCallException extends LogicException
|- BadMethodCallException extends BadFunctionCallException
|- DomainException extends LogicException
|- InvalidArgumentException extends LogicException
|- LengthException extends LogicException
|- OutOfRangeException extends LogicException
|- PharException extends Exception
|- ReflectionException extends Exception
|- RuntimeException extends Exception
|- OutOfBoundsException extends RuntimeException
|- OverflowException extends RuntimeException
|- PDOException extends RuntimeException
|- RangeException extends RuntimeException
|- UnderflowException extends RuntimeException
|- UnexpectedValueException extends RuntimeException

Ошибка, почему?

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

Проблема заключалась в том, что было несколько фатальных ошибок, которые не могли быть обработаны определяемым пользователем обработчиком ошибок. Это означало, что вы не могли корректно обрабатывать фатальные ошибки в PHP. Было несколько побочных эффектов, которые были проблематичными, такие как потеря контекста времени выполнения, деструкторы не вызывались, да и вообще иметь дело с ними было неудобно. В PHP 7 фатальные ошибки теперь являются исключениями, и мы можем легко их обработать. Фатальные ошибки приводят к возникновению исключений. Вам необходимо обрабатывать нефатальные ошибки с помощью функции обработки ошибок.

Вот пример ловли фатальной ошибки в PHP 7.1. Обратите внимание, что нефатальная ошибка не обнаружена.

<?php 

try {
// будет генерировать уведомление, которое не будет поймано
echo $someNotSetVariable;
// фатальная ошибка, которая сейчас на самом деле ловится
someNoneExistentFunction();
} catch (Error $e) {
echo "Error caught: " . $e->getMessage();
}

Этот скрипт выведет сообщение об ошибке при попытке доступа к недопустимой переменной. Попытка вызвать функцию, которая не существует, приведет к фатальной ошибке в более ранних версиях PHP, но в PHP 7.1 вы можете ее перехватить. Вот вывод для скрипта:

Notice: Undefined variable: someNotSetVariable on line 3
Error caught: Call to undefined function someNoneExistentFunction()

Константы ошибок

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

Вот некоторые из наиболее часто встречающихся кодов ошибок:

  • E_DEPRECATED — интерпретатор сгенерирует этот тип предупреждений, если вы используете устаревшую языковую функцию. Сценарий обязательно продолжит работать без ошибок.
  • E_STRICT — аналогично E_DEPRECATED, — указывает на то, что вы используете языковую функцию, которая не является стандартной в настоящее время и может не работать в будущем. Сценарий будет продолжать работать без каких-либо ошибок.
  • E_PARSE — ваш синтаксис не может быть проанализирован, поэтому ваш скрипт не запустится. Выполнение скрипта даже не запустится.
  • E_NOTICE — движок просто выведет информационное сообщение. Выполнение скрипта не прервется, и ни одна из ошибок не будет выдана.
  • E_ERROR — скрипт не может продолжить работу, и завершится. Выдает ошибки, а как они будут обрабатываться, зависит от обработчика ошибок.
  • E_RECOVERABLE_ERROR — указывает на то, что, возможно, произошла опасная ошибка, и движок работает в нестабильном состоянии. Дальнейшее выполнение зависит от обработчика ошибок, и ошибка обязательно будет выдана.

Полный список констант можно найти в руководстве по PHP.

Функция обработчика ошибок

Функция set_error_handler() используется, чтобы сообщить PHP как обрабатывать стандартные ошибки, которые не являются экземплярами класса исключений Error. Вы не можете использовать функцию обработчика ошибок для фатальных ошибок. Исключения ошибок должны обрабатываться с помощью операторов try/catch. set_error_handler() принимает callback функцию в качестве своего параметра. Callback-функции в PHP могут быть заданы двумя способами: либо строкой, обозначающей имя функции, либо передачей массива, который содержит объект и имя метода (именно в этом порядке). Вы можете указать защищенные и приватные методы для callable в объекте. Вы также можете передать значение null, чтобы указать PHP вернуться к использованию стандартного механизма обработки ошибок. Если ваш обработчик ошибок не завершает программу и возвращает результат, ваш сценарий будет продолжать выполняться со строки, следующей за той, где произошла ошибка.

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

Вот пример:

<?php

function myCustomErrorHandler(int $errNo, string $errMsg, string $file, int $line) {
echo "Ух ты, мой обработчик ошибок получил #[$errNo] в [$file] на [$line]: [$errMsg]";
}

set_error_handler('myCustomErrorHandler');

try {
why;
} catch (Throwable $e) {
echo 'И моя ошибка: ' . $e->getMessage();
}

Если вы запустите этот код в PHP-консоли php -a, вы должны получить похожий вывод:

Error #[2] occurred in [php shell code] at line [3]: [Use of undefined constant why - assumed 'why' (this will throw an Error in a future version of PHP)]

Самые известные PHP библиотеки , которые делают обширное использование РНР set_error_handler() и могут сделать хорошие представления исключений и ошибок являются whoops,  и Symony Debug, ErrorHandler компоненты. Я смело рекомендую использовать один из них. Если не собираетесь их использовать в своем проекте, то вы всегда можете черпать вдохновение из их кода. В то время как компонент Debug широко используется в экосистеме Symfony, Whoops остается библиотекой выбора для фреймворка Laravel . 

Для подробного и расширенного использования, пожалуйста, обратитесь к руководству по PHP для обработчика ошибок.

Отображение или подавление нефатальной ошибки

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

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

Для этого вам нужно настроить PHP, используя следующие параметры в вашем файле php.ini:

  • display_errors – может быть установлен в false для подавления сообщений
  • log_errors – может использоваться для хранения сообщений об ошибках в файлах журнала
  • error_reporting – можно настроить, какие ошибки вызывают отчет

Лучше всего корректно обрабатывать ошибки в вашем приложении. В производственном процессе вы должны скорее регистрировать необработанные ошибки, чем разрешать их отображение пользователю. Функция error_log() может использоваться для отправки сообщения одной из определенных процедур обработки ошибок. Вы также можете использовать функцию error_log() для отправки электронных писем, но лично вы бы предпочли использовать хорошее решение для регистрации ошибок и получения уведомлений при возникновении ошибок, например Sentry или Rollbar .

Существует вещь, называемая оператором контроля ошибок ( @ ), который по своей сути может игнорировать и подавлять ошибки. Использование очень простое — просто добавьте любое выражение PHP с символом «собаки», и сгенерированная ошибка будет проигнорирована. Хотя использование этого оператора может показаться интересным, я призываю вас не делать этого. Мне нравится называть это живым пережитком прошлого.

Больше информации для всех функций, связанных с ошибками PHP, можно найти в руководстве.

Исключения (Exceptions)

Исключения являются основной частью объектно-ориентированного программирования и впервые были представлены в PHP 5.0. Исключением является состояние программы, которое требует специальной обработки, поскольку оно не выполняется ожидаемым образом. Вы можете использовать исключение, чтобы изменить поток вашей программы, например, чтобы прекратить что-либо делать, если некоторые предварительные условия не выполняются.

Исключение будет возникать в стеке вызовов, если вы его не перехватите. Давайте посмотрим на простой пример:

try {
print "это наш блок попыток n";
throw new Exception();
} catch (Exception $e) {
print "что-то пошло не так, есть улов!";
} finally {
print "эта часть всегда выполняется";
}

PHP включает в себя несколько стандартных типов исключений, а стандартная библиотека PHP (SPL) включает в себя еще несколько. Хотя вам не нужно использовать эти исключения, это означает, что вы можете использовать более детальное обнаружение ошибок и отчеты. Классы Exception и Error реализуют интерфейс Throwable и, как и любые другие классы, могут быть расширены. Это позволяет вам создавать гибкие иерархии ошибок и адаптировать обработку исключений. Только класс, который реализует класс Throwable, может использоваться с ключевым словом throw. Другими словами, вы не можете объявить свой собственный базовый класс и затем выбросить его как исключение.

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

Ловля исключений

Вы должны использовать try/catch структуру:

<?php

class MyCustomException extends Exception { }

function throwMyCustomException() {
throw new MyCustomException('Здесь что-то не так.');
}

try {
throwMyCustomException();
} catch (MyCustomException $e) {
echo "Ваше пользовательское исключение поймано";
echo $e->getMessage();
} catch (Exception $e) {
echo "Стандартное исключение PHP";
}

Как видите, есть два предложения catch. Исключения будут сопоставляться с предложениями сверху вниз, пока тип исключения не будет соответствовать предложению catch. Эта очень простая функция throwMyCustomException() генерирует исключение MyCustomException, и мы ожидаем, что оно будет перехвачено в первом блоке. Любые другие исключения, которые произойдут, будут перехвачены вторым блоком. Здесь мы вызываем метод getMessage() из базового класса Exception. Вы можете найти больше информации о дополнительном методе в Exception PHP docs.

Кроме того, можно указать несколько исключений, разделяя их трубой ( | ).

Давайте посмотрим на другой пример:

<?php

class MyCustomException extends Exception { }
class MyAnotherCustomException extends Exception { }

try {
throw new MyAnotherCustomException;
} catch (MyCustomException | MyAnotherCustomException $e) {
echo "Caught : " . get_class($e);
}

Этот очень простой блок catch будет перехватывать исключения типа MyCustomException и MyAnotherCustomException.

Немного более продвинутый сценарий:

// exceptions.php
use SymfonyComponentHttpKernelExceptionNotFoundHttpException;

try {
throw new NotFoundHttpException();
} catch (Exception $e) {
echo 1;
} catch (NotFoundHttpException $e) {
echo 2;
} catch (Exception $e) {
echo 3;
} finally {
echo 4;
}

Это ваш окончательный ответ?

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

<?php

class MyCustomException extends Exception { }

function throwMyCustomException() {
throw new MyCustomException('Здесь что-то не так');
}

try {
throwMyCustomException();
} catch (MyCustomException $e) {
echo "Ваше пользовательское исключение поймано ";
echo $e->getMessage();
} catch (Exception $e) {
echo "Стандартное исключение PHP";
} finally {
echo "Я всегда тут";
}

Вот хороший пример того, как работают операторы PHP catch/finally:

<?php

try {
try {
echo 'a-';
throw new exception();
echo 'b-';
} catch (Exception $e) {
echo 'пойманный-';
throw $e;
} finally {
echo 'завершенный-';
}
} catch (Exception $e) {
echo 'конец-';
}

Функция обработчика исключений

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

Для этого вы используете функцию set_exception_handler() , которая принимает вызываемый элемент в качестве параметра. Ваш сценарий завершится после того, как вызов будет выполнен.

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

<?php

class MyCustomException extends Exception { }

function exception_handler($exception) {
echo "Uncaught exception: " , $exception->getMessage(), "n";
}

set_exception_handler('exception_handler');

try {
throw new Exception('Uncaught Exception');
} catch (MyCustomException $e) {
echo "Ваше пользовательское исключение поймано ";
echo $e->getMessage();
} finally {
echo "Я всегда тут";
}

print "Не выполнено";

Здесь простая функция exception_handler будет выполняться после блока finally, когда ни один из типов исключений не был сопоставлен. Последний вывод никогда не будет выполнен.

Для получения дополнительной информации обратитесь к документации PHP. 

Старый добрый «T_PAAMAYIM_NEKUDOTAYIM»

Вероятно, это было самое известное сообщение об ошибке PHP. В последние годы было много споров по этому поводу. Вы можете прочитать больше в отличном сообщении в блоге от Фила Осетра .  

Сегодня я с гордостью могу сказать, что если вы запустите этот код с PHP 7, то сообщение о T_PAAMAYIM_NEKUDOTAYIM больше не будет:

<?php

class foo
{
static $bar = 'baz';
}

var_dump('foo'::$bar);

// Output PHP < 7.0:
// PHP Parse error: syntax error, unexpected '::' (T_PAAMAYIM_NEKUDOTAYIM) in php shell code on line 1
// Output PHP > 7.0:
// string(3) "baz"
?>

В заключении

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

Исключения в программировании (exceptions) — это механизм, который позволяет программе обрабатывать нетипичную ситуацию и при этом не прекращать работу. Благодаря этому механизму разработчик может описать в коде реакцию программы на такие ситуации.

Простой пример: в программе-калькуляторе исключением может стать ситуация, когда пользователь решит поделить на ноль. Это не должно стать ошибкой, из-за которой рушится вся программа, но чтобы ситуация не застопорила исполнение остального кода, нужно ее правильно обработать. Для этого необходимы обработчики исключений. Они позволяют «сказать» программе, что ей делать, если такое случится.

Механизм обработки исключений существует в большинстве языков программирования. Он может быть реализован немного по-разному, но общая суть схожа: это всегда какие-то особые случаи, которые надо обработать отдельно. Мы при описании будем отталкиваться от особенностей исключений в Java, но встретить их можно и в других языках: JavaScript, PHP, Python, C++ и так далее.

Зачем нужны исключения

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

Работа без исключений

Исключения нужны, чтобы программа продолжала относительно корректно работать, даже если что-то пошло не так.

Работа с исключениями

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

Исключения делятся на две большие группы, которые пересекаются друг с другом: синхронные и асинхронные. Синхронные могут возникнуть только в конкретном месте программы или при выполнении определенной операции: открытие файла, деление и так далее. Асинхронные могут возникнуть когда и где угодно. Их «ловят» по-разному, чтобы успешно отслеживать и те, и другие.

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

Как происходит работа с исключениями

  • Разработчик пишет код и понимает, что в какой-то момент в том или ином месте может возникнуть нештатная ситуация. Бывает, что исключения добавляют в уже написанный код — например, нештатную ситуацию обнаружили при тестировании.
  • В этом месте пишется особый блок кода — обработчик. Он говорит программе: здесь может возникнуть особая ситуация, если она случится, выполни вот это.
  • Внутри обработчика — функция, которая выполнится, если программа столкнется с описанной ситуацией. Она или исправит ситуацию, или скорректирует дальнейшее выполнение программы.

Бывают исключения, которые нельзя предусмотреть. Разработчики обрабатывают не все возможные нештатные ситуации, а только самые очевидные, чтобы не перегружать код. Это справедливо для большинства сфер разработки, кроме тех, где слишком высока цена ошибки.

Как устроена обработка исключений

Существуют разные виды обработки: структурная и неструктурная, с возвратом и без возврата. Они различаются механизмом действия, но общая суть одна: это функция, которая запускается, если в коде случилась та или иная исключительная ситуация. Тут можно использовать условный оператор if или специальные синтаксические конструкции.

В примере с делением на ноль обработчик может отменить попытку деления и сказать пользователю, что на ноль делить нельзя, — но это самый простой пример. В реальности все сложнее.

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

Вот пример: не запустился скрипт, необходимый для работы следующего скрипта. Следующий скрипт заметил это, зафиксировал исключение и обратился к обработчику, который запустил нужный скрипт «вручную». После этого все может работать, как и было задумано.

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

Структурная и неструктурная обработка. Это два способа подключить обработчики. В первом случае они встраиваются в код, а когда генерируется исключение, для него выбирается тот или иной обработчик в зависимости от ситуации. Во втором случае обработчики существуют отдельно и «подключаются» к конкретным видам исключений с помощью специальных команд. Способ выбирается в зависимости от вида исключения, особенностей кода и языка.

Обычно асинхронные исключения обрабатывают неструктурно, а синхронные — структурно.

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

Исключения и ошибки: разница

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

  • ошибка означает, что программа «упала», что ее работу нельзя продолжить и она должна быть завершена. Ошибку невозможно исправить — только сообщить о ней пользователю, записать в лог и прекратить исполнение кода;
  • исключение — это нештатная ситуация, которую тем не менее можно попробовать починить «на ходу», не закрывая программу. В этом есть смысл, в отличие от ситуации с ошибкой.

Это действительно похожие понятия. В Java, например, сущности исключений и ошибок наследуются от общего предка — интерфейса Throwable. Но ошибка — это явление, когда что-то сделать принципиально не получается. А исключение — ситуация, когда программа просто не знает, что делать, если не указать на это дополнительно.

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

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

Когда пользоваться исключениями, а когда — ошибками

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

Обработчики ошибок советуют использовать тогда, когда проблема не решаема изнутри программы. Например, у приложения нет связи с сервером — оно не может продолжать работу без этого. Или какие-то критичные файлы оказались повреждены, и из-за этого код просто нельзя исполнить. Или в системе закончилась свободная память. Это никак не поправить программными способами.

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

Как начать пользоваться исключениями

В большинстве языков механизм обработки исключений есть по умолчанию — это популярная функция. Но приступать к работе с ними рекомендуют после изучения базовых возможностей языка. Мы советуем идти от простого к сложному: начать с основ и затем переходить к комплексным темам. Конкретно обработка исключений обычно изучается перед тем, как человек переходит к практическим проектам, потому что любая более-менее сложная программа может столкнуться с исключениями в ходе работы.

Понравилась статья? Поделить с друзьями:
  • Правильная норма лексическая ошибка
  • Правило соразмерности ошибки
  • Правило сложения ошибок
  • Правило ошибки выжившего
  • Ппд 14тс 10 коды ошибок