Обработка исключений
Обрабо́тка исключи́тельных ситуа́ций (англ. exception handling) — механизм языков программирования, предназначенный для описания реакции программы на ошибки времени выполнения и другие возможные проблемы (исключения), которые могут возникнуть при выполнении программы и приводят к невозможности (бессмысленности) дальнейшей отработки программой её базового алгоритма. В русском языке также применяется более короткая форма термина: «обработка исключений».
Исключения
Общее понятие исключительной ситуации
Во время выполнения программы могут возникать ситуации, когда состояние внешних данных, устройств ввода-вывода или компьютерной системы в целом делает дальнейшие вычисления в соответствии с базовым алгоритмом невозможными или бессмысленными. Классические примеры подобных ситуаций приведены ниже.
- Целочисленное деление на ноль. Конечного результата у данной операции быть не может, поэтому ни дальнейшие вычисления, ни попытка использования результата деления не приведут к решению задачи.
- Ошибка при попытке считать данные с внешнего устройства. Если данные не удаётся получить, любые дальнейшие запланированные операции с ними бессмысленны.
- Исчерпание доступной памяти. Если в какой-то момент система оказывается не в состоянии выделить достаточный для прикладной программы объём оперативной памяти, программа не сможет работать нормально.
- Появление сигнала аварийного отключения электропитания системы. Прикладную задачу, по всей видимости, решить не удастся, в лучшем случае (при наличии какого-то резерва питания) прикладная программа может позаботиться о сохранении данных.
Виды исключительных ситуаций
Исключительные ситуации, возникающие при работе программы, можно разделить на два основных типа: синхронные и асинхронные, принципы реакции на которые существенно различаются.
- Синхронные исключения могут возникнуть только в определённых, заранее известных точках программы. Так, ошибка чтения файла или коммуникационного канала, нехватка памяти — типичные синхронные исключения, так как возникают они только в операции чтения или в операции выделения памяти соответственно.
- Асинхронные исключения могут возникать в любой момент времени и не зависят от того, какую конкретно инструкцию программы выполняет система. Типичные примеры таких исключений: аварийный отказ питания или поступление новых данных.
Некоторые типы исключений могут быть отнесены как к синхронным, так и к асинхронным. Например, инструкция деления на ноль формально должна приводить к синхронному исключению, так как логически оно возникает именно при выполнении данной команды, но на некоторых платформах за счёт глубокой конвейеризации исключение может фактически оказаться асинхронным.
Обработчики исключений
Общее описание
В отсутствие собственного механизма обработки исключений для прикладных программ наиболее общей реакцией на любую исключительную ситуацию является немедленное прекращение выполнения с выдачей пользователю сообщения о характере исключения. Можно сказать, что в подобных случаях единственным и универсальным обработчиком исключений становится операционная система. Например, в операционную систему Windows встроена утилита Dr. Watson, которая занимается сбором информации о необработанном исключении и её отправкой на специальный сервер компании Microsoft.
Возможно игнорирование исключительной ситуации и продолжение работы, но такая тактика опасна, так как приводит к ошибочным результатам работы программ или возникновению ошибок впоследствии. Например, проигнорировав ошибку чтения из файла блока данных, программа получит в своё распоряжение не те данные, которые она должна была считать, а какие-то другие. Результаты их использования предугадать невозможно.
Обработка исключительных ситуаций самой программой заключается в том, что при возникновении исключительной ситуации управление передаётся некоторому заранее определённому обработчику — блоку кода, процедуре, функции, которые выполняют необходимые действия.
Существует два принципиально разных механизма функционирования обработчиков исключений.
- Обработка с возвратом подразумевает, что обработчик исключения ликвидирует возникшую проблему и приводит программу в состояние, когда она может работать дальше по основному алгоритму. В этом случае после того, как выполнится код обработчика, управление передаётся обратно в ту точку программы, где возникла исключительная ситуация (либо на команду, вызвавшую исключение, либо на следующую за ней, как в некоторых старых диалектах языка BASIC) и выполнение программы продолжается. Обработка с возвратом типична для обработчиков асинхронных исключений (которые обычно возникают по причинам, не связанным прямо с выполняемым кодом), для обработки синхронных исключений она малопригодна.
- Обработка без возврата заключается в том, что после выполнения кода обработчика исключения управление передаётся в некоторое, заранее заданное место программы, и с него продолжается исполнение. То есть, фактически, при возникновении исключения команда, во время работы которой оно возникло, заменяется на безусловный переход к заданному оператору.
Существует два варианта подключения обработчика исключительных ситуаций к программе: структурная и неструктурная обработка исключений.
Неструктурная обработка исключений
Неструктурная обработка исключений реализуется в виде механизма регистрации функций или команд-обработчиков для каждого возможного типа исключения. Язык программирования или его системные библиотеки предоставляют программисту как минимум две стандартные процедуры: регистрации обработчика и разрегистрации обработчика. Вызов первой из них «привязывает» обработчик к определённому исключению, вызов второй — отменяет эту «привязку». Если исключение происходит, выполнение основного кода программы немедленно прерывается и начинается выполнение обработчика. По завершении обработчика управление передаётся либо в некоторую наперёд заданную точку программы, либо обратно в точку возникновения исключения (в зависимости от заданного способа обработки — с возвратом или без). Независимо от того, какая часть программы в данный момент выполняется, на определённое исключение всегда реагирует последний зарегистрированный для него обработчик. В некоторых языках зарегистрированный обработчик сохраняет силу только в пределах текущего блока кода (процедуры, функции), тогда процедура разрегистрации не требуется. Ниже показан условный фрагмент кода программы с неструктурной обработкой исключений:
УстановитьОбработчик(ОшибкаБД, ПерейтиНа ОшБД) // На исключение "ОшибкаБД" установлен обработчик - команда "ПерейтиНа ОшБД" ... // Здесь находятся операторы работы с БД ПерейтиНа СнятьОшБД // Команда безусловного перехода - обход обработчика исключений ОшБД: // метка - сюда произойдёт переход в случае ошибки БД по установленному обработчику ... // Обработчик исключения БД СнятьОшБД: // метка - сюда произойдёт переход, если контролируемый код выполнится без ошибки БД. СнятьОбработчик(ОшибкаБД) // Обработчик снят
Неструктурная обработка — практически единственный вариант для обработки асинхронных исключений, но для синхронных исключений она неудобна: приходится часто вызывать команды установки/снятия обработчиков, всегда остаётся опасность нарушить логику работы программы, пропустив регистрацию или разрегистрацию обработчика.
Структурная обработка исключений
Структурная обработка исключений требует обязательной поддержки со стороны языка программирования — наличия специальных синтаксических конструкций. Такая конструкция содержит блок контролируемого кода и обработчик (обработчики) исключений. Наиболее общий вид такой конструкции (условный):
НачалоБлока ... // Контролируемый код ... если (условие) то СоздатьИсключение Исключение2 ... Обработчик Исключение1 ... // Код обработчика для Исключения1 Обработчик Исключение2 ... // Код обработчика для Исключения2 ОбработчикНеобработанных ... // Код обработки ранее не обработанных исключений КонецБлока
Здесь «НачалоБлока» и «КонецБлока» — ключевые слова, которые ограничивают блок контролируемого кода, а «Обработчик» — начало блока обработки соответствующего исключения. Если внутри блока, от начала до первого обработчика, произойдёт исключение, то произойдёт переход на обработчик, написанный для него, после чего весь блок завершится и исполнение будет продолжено со следующей за ним команды. В некоторых языках нет специальных ключевых слов для ограничения блока контролируемого кода, вместо этого обработчик (обработчики) исключений могут быть встроены в некоторые или во все синтаксические конструкции, объединяющие несколько операторов. Так, например, в языке Ада любой составной оператор (begin — end) может содержать обработчик исключений.
«ОбработчикНеобработанных» — это обработчик исключений, которые не соответствуют ни одному из описанных выше в данном блоке. Обработчики исключений в реальности могут описываться по-разному (один обработчик на все исключения, по одному обработчику на каждый тип исключения), но принципиально они работают одинаково: при возникновении исключения находится первый соответствующий ему обработчик в данном блоке, его код выполняется, после чего выполнение блока завершается. Исключения могут возникать как в результате программных ошибок, так и путём явной их генерации с помощью соответствующей команды (в примере — команда «СоздатьИсключение»). С точки зрения обработчиков такие искусственно созданные исключения ничем не отличаются от любых других.
Блоки обработки исключений могут многократно входить друг в друга, как явно (текстуально), так и неявно (например, в блоке вызывается процедура, которая сама имеет блок обработки исключений). Если ни один из обработчиков в текущем блоке не может обработать исключение, то выполнение данного блока немедленно завершается, и управление передаётся на ближайший подходящий обработчик более высокого уровня иерархии. Это продолжается до тех пор, пока обработчик не найдётся и не обработает исключение или пока исключение не выйдет из обработчиков заданных программистом и не будет передано системному обработчику по умолчанию, аварийно закроющему программу.
Иногда бывает неудобно завершать обработку исключения в текущем блоке, то есть желательно, чтобы при возникновении исключения в текущем блоке обработчик выполнил какие-то действия, но исключение продолжило бы обрабатываться на более высоком уровне (обычно так бывает, когда обработчик данного блока не полностью обрабатывает исключение, а лишь частично). В таких случаях в обработчике исключений генерируется новое исключение или возобновляется, с помощью специальной команды, ранее появившееся. Код обработчиков не является защищённым в данном блоке, поэтому созданное в нём исключение будет обрабатываться в блоках более высокого уровня.
Блоки с гарантированным завершением
Помимо блоков контролируемого кода для обработки исключений, языки программирования могут поддерживать блоки с гарантированным завершением. Их использование оказывается удобным тогда, когда в некотором блоке кода, независимо от того, произошли ли какие-то ошибки, необходимо перед его завершением выполнить определённые действия. Простейший пример: если в процедуре динамически создаётся какой-то локальный объект в памяти, то перед выходом из этой процедуры объект должен быть уничтожен (чтобы избежать утечки памяти), независимо от того, произошли после его создания ошибки или нет. Такая возможность реализуется блоками кода вида:
НачалоБлока ... // Основной код Завершение ... // Код завершения КонецБлока
Заключённые между ключевыми словами «НачалоБлока» и «Завершение» операторы (основной код) выполняются последовательно. Если при их выполнении не возникает исключений, то затем выполняются операторы между ключевыми словами «Завершение» и «КонецБлока» (код завершения). Если же при выполнении основного кода возникает исключение (любое), то сразу же выполняется код завершения, после чего весь блок завершается, а возникшее исключение продолжает существовать и распространяться до тех пор, пока его не перехватит какой-либо блок обработки исключений более высокого уровня.
Принципиальное отличие блока с гарантированным завершением от обработки — то, что он не обрабатывает исключение, а лишь гарантирует выполнение определённого набора операций перед тем, как включится механизм обработки. Нетрудно заметить, что блок с гарантированным завершением легко реализуется с помощью обычного механизма структурной обработки (для этого достаточно поставить команду генерации исключения непосредственно перед завершением контролируемого блока и правильно написать код обработчика), но наличие отдельной конструкции позволяет сделать код более прозрачным и защищает от случайных ошибок.
Поддержка в различных языках
Большинство современных языков программирования, такие как Ada, C++, D, Delphi, Objective-C, Java, JavaScript, Eiffel, OCaml, Ruby, Python, Common Lisp, SML, PHP, все языки платформы .NET и др., имеет встроенную поддержку структурной обработки исключений. В этих языках при возникновении исключения, поддерживаемого языком, происходит раскрутка стека вызовов до первого обработчика исключений подходящего типа, и управление передаётся обработчику.
За исключением незначительных различий в синтаксисе существует лишь пара вариантов обработки исключений. В наиболее распространённом из них исключительная ситуация генерируется специальным оператором (throw
или raise
), а само исключение, с точки зрения программы, представляет собой некоторый объект данных. То есть, генерация исключения состоит из двух этапов: создания объекта-исключения и возбуждения исключительной ситуации с этим объектом в качестве параметра. При этом конструирование такого объекта само по себе выброса исключения не вызывает. В одних языках объектом-исключением может быть объект любого типа данных (в том числе строкой, числом, указателем и так далее), в других — только предопределённого типа-исключения (чаще всего он имеет имя Exception
) и, возможно, его производных типов (типов-потомков, если язык поддерживает объектные возможности).
Область действия обработчиков начинается специальным ключевым словом try
или просто языковым маркером начала блока (например, begin
) и заканчивается перед описанием обработчиков (catch
, except
, resque
). Обработчиков может быть несколько, один за одним, и каждый может указывать тип исключения, который он обрабатывает. Как правило, никакого подбора наиболее подходящего обработчика не производится, и выполняется первый же обработчик, совместимый по типу с исключением. Поэтому порядок следования обработчиков имеет важное значение: если обработчик, совместимый с многими или всеми типами исключений, окажется в тексте прежде специфических обработчиков для конкретных типов, то специфические обработчики не будут использоваться вовсе.
Некоторые языки также допускают специальный блок (else
), который выполняется, если ни одного исключения не было сгенерировано в соответствующей области действия. Чаще встречается возможность гарантированного завершения блока кода (finally
, ensure
). Заметным исключением является Си++, где такой конструкции нет. Вместо неё используется автоматический вызов деструкторов объектов. Вместе с тем существуют нестандартные расширения Си++, поддерживающие и функциональность finally
(например в MFC).
В целом обработка исключений может выглядеть следующим образом (в некотором абстрактном языке):
try {
line = console.readLine();
if (line.length() == 0)
throw new EmptyLineException("Строка, считанная из консоли, пустая!");
console.printLine("Привет, %s!" % line);
}
catch (EmptyLineException exception) {
console.printLine("Привет!");
}
catch (Exception exception) {
console.printLine("Ошибка: " + exception.message());
}
else {
console.printLine("Программа выполнилась без исключительных ситуаций");
}
finally {
console.printLine("Программа завершается");
}
В некоторых языках может быть лишь один обработчик, который разбирается с различными типами исключений самостоятельно.
Достоинства и недостатки
Достоинства использования исключений особенно заметно проявляются при разработке библиотек процедур и программных компонентов, ориентированных на массовое использование. В таких случаях разработчик часто не знает, как именно должна обрабатываться исключительная ситуация (при написании универсальной процедуры чтения из файла невозможно заранее предусмотреть реакцию на ошибку, так как эта реакция зависит от использующей процедуру программы), но ему это и не нужно — достаточно сгенерировать исключение, обработчик которого предоставляется реализовать пользователю компонента или процедуры. Единственная альтернатива исключениям в таких случаях — возврат кодов ошибок, которые вынужденно передаются по цепочке между несколькими уровнями программы, пока не доберутся до места обработки, загромождая код и снижая его понятность. Использование исключений в целях контроля ошибок повышает читаемость кода, так как позволяет отделить обработку ошибок от самого алгоритма, и облегчает программирование и использование компонентов других разработчиков. А обработка ошибок может быть централизована в аспектах.
К сожалению, реализация механизма обработки исключений существенно зависит от языка, и даже компиляторы одного и того же языка на одной и той же платформе могут иметь значительные различия. Это не позволяет прозрачно передавать исключения между частями программы, написанной на разных языках; например, поддерживающие исключения библиотеки обычно непригодны для использования в программах на языках, отличных от тех, для которых они разработаны, и, тем более, на языках, не поддерживающих механизм обработки исключений. Такое состояние существенно ограничивает возможности использования исключений, например, в ОС UNIX и её клонах и под Windows, так как большинство системного ПО и низкоуровневых библиотек этих систем пишется на языке Си, не поддерживающем исключений. Соответственно, для работы с API таких систем с применением исключений приходится писать библиотеки-обёртки, функции которых анализировали бы коды возврата функций API и в нужных случаях генерировали бы исключения.
Поддержка исключений усложняет язык и компилятор. Также она снижает скорость работы программы, так как стоимость обработки исключения, как правило, выше стоимости обработки кода ошибки. Поэтому в местах программы, критичных по скорости, не рекомендуют возбуждать и обрабатывать исключения, хотя следует отметить, что в прикладном программировании случаи, когда разница в скорости обработки исключений и кодов возврата действительно существенна, очень редки.
Корректная реализация исключений может быть затруднительной в языках с автоматическим вызовом деструкторов. При возникновении исключения в блоке необходимо автоматически вызвать деструкторы объектов, созданных в данном блоке, но только тех, которые не были ещё удалены обычным порядком. Кроме того, требование прерывания текущей операции при возникновении исключения вступает в противоречие с требованием обязательного автоматического удаления в языках с автодеструкторами: если исключение возникнет в деструкторе, то либо компилятор будет вынужден удалить не полностью освобождённый объект, либо объект останется существующим, то есть возникнет утечка памяти. Вследствие этого генерация неперехватываемых исключений в деструкторах в ряде случаев просто запрещается.
Джоэл Спольски считает, что код, рассчитанный на работу с исключениями, теряет линейность и предсказуемость. Если в классическом коде выходы из блока, процедуры или функции находятся только там, где их явно указал программист, то в коде с исключениями исключение (потенциально) может произойти в любом операторе и анализом самого кода невозможно узнать, где именно исключения могут происходить. В коде же, рассчитанном на исключения, предсказать, в каком месте произойдёт выход из блока кода, невозможно, и любой оператор должен рассматриваться как потенциально последний в блоке, в результате сложность кода возрастает, а надёжность снижается.[1]
Также в сложных программах возникают большие «нагромождения» операторов try ... finally
и try ... catch
(try ... except
), если не использовать аспекты.
Проверяемые исключения
Некоторые проблемы простой обработки исключений
Изначально (например, в C++) не существовало никакой формальной дисциплины описания, генерации и обработки исключений: любое исключение может быть возбуждено в любом месте программы, и, если для него не находится обработчика в стеке вызовов, выполнение программы прерывается аварийно. Если функция (особенно библиотечная) генерирует исключения, то для устойчивой работы использующая её программа должна перехватывать их все. Когда по какой-либо причине одно из возможных исключений оказывается необработанным, будет происходить неожиданное аварийное завершение программы.
С подобными эффектами можно бороться организационными мерами: описывая возможные исключения, возникающие в библиотечных модулях, в соответствующей документации. Но при этом всегда остаётся вероятность пропустить необходимый обработчик из-за случайной ошибки или несоответствия документации коду (что вовсе не редкость). Чтобы полностью исключить потерю обработки исключений, в обработчики приходится специально добавлять ветвь обработки «всех остальных» исключений (которая гарантированно перехватит любые, даже заранее неизвестные исключения), но такой выход не всегда оптимален. Более того, сокрытие всех возможных исключений может привести к ситуации, когда будут скрыты серьёзные и при этом трудно обнаруживаемые ошибки.
Механизм проверяемых исключений
Позже в ряде языков, например, в Java, появились проверяемые исключения. Сущность этого механизма состоит в добавлении в язык следующих правил и ограничений:
- В описании функции (или метода класса) в явном виде перечисляются все типы исключений, которые она может сгенерировать.
- Функция, вызывающая функцию или метод с объявленными исключениями, для каждого из этих исключений обязана либо содержать обработчик, либо, в свою очередь, указывать этот тип как генерируемый ею в своём описании.
- Компилятор проверяет наличие обработчика в теле функции или записи исключения в её заголовке. Реакция на наличие неописанного и необработанного исключения может быть разной. Например, в Java, если компилятор обнаруживает возможность возникновения исключения, которое не описано в заголовке функции и не обрабатывается в ней, программа считается некорректной и не компилируется. В C++ возникновение в функции неописанного и необработанного исключения приводит к немедленному завершению программы; при этом отсутствие у функции списка объявленных исключений обозначает возможность возникновения любых исключений и стандартный порядок их обработки внешним кодом.
Внешне (в языке Java) реализация такого подхода выглядит следующим образом:
int getVarValue(String varName) throws SQLException {
... // код метода, возможно, содержащий вызовы, способные бросить исключение SQLException
}
// Ошибка при компиляции - исключение не объявлено и не перехвачено
int eval1(String expression) {
...
int a = prev + getVarValue("abc");
...
}
// Правильно — исключение объявлено и будет передаваться дальше
int eval2(String expression) throws SQLException {
...
int a = prev + getVarValue("abc");
...
}
// Правильно — исключение перехватывается внутри метода и наружу не выходит
int eval3(String expression) {
...
try {
int a = prev + getVarValue("abc");
} catch (SQLException ex) {
// Обработка исключения
}
...
}
Здесь метод getVarValue объявлен как генерирующий исключение SQLException. Следовательно, любой использующий его метод должен либо перехватить это исключение, либо объявить его как генерируемое. В данном примере метод eval1 приведёт к ошибке компиляции, поскольку вызывает метод getVarValue, но не перехватывает исключение и не объявляет его. Метод eval2 объявляет исключение, а метод eval3 перехватывает и обрабатывает его, оба этих метода корректны в отношении работы с исключением, вызываемым методом getVarValue.
Преимущества и недостатки
Проверяемые исключения снижают количество ситуаций, когда исключение, которое могло быть обработано, вызвало критическую ошибку в программе, поскольку за наличием обработчиков следит компилятор. Это особенно полезно при изменениях кода, когда метод, который не мог ранее выбрасывать исключение типа X, начинает это делать; компилятор автоматически отследит все случаи его использования и проверит наличие соответствующих обработчиков.
Другим полезным качеством проверяемых исключений является то, что они способствуют осмысленному написанию обработчиков: программист явно видит полный и правильный список исключений, которые могут возникнуть в данном месте программы, и может написать на каждое из них осмысленный обработчик вместо того, чтобы создавать «на всякий случай» общий обработчик всех исключений, одинаково реагирующий на все нештатные ситуации.
У проверяемых исключений есть и недостатки.
- Они вынуждают создавать обработчики исключений, с которыми программист в принципе справиться не может, например ошибок ввода-вывода в веб-приложении. Это приводит к появлению «глупых» обработчиков, которые не делают ничего или дублируют системный обработчик критической ошибки (например, выводят стек вызова исключения) и, в итоге, только замусоривают код.
- Становится невозможным добавление нового проверяемого исключения в метод, описанный в библиотеке, поскольку это нарушает обратную совместимость. (Это верно и для небиблиотечных методов, но в этом случае проблема менее существенна, так как весь код, в конечном итоге, доступен и может быть переработан).
Из-за перечисленных недостатков при обязательности использования проверяемых исключений этот механизм часто обходят. Например, многие библиотеки объявляют все методы как выбрасывающие некоторый общий класс исключений (например, Exception
), и только на этот тип исключения создаются обработчики. Результатом становится то, что компилятор заставляет писать обработчики исключений даже там, где они объективно не нужны, и становится невозможно определить без чтения исходников, какие именно подклассы декларируемых исключений бросает метод, чтобы навесить на них разные обработчики. Более правильным подходом считается перехват внутри метода новых исключений, порождённых вызываемым кодом, а при необходимости передать исключение дальше — «заворачивание» его в исключение, уже возвращаемое методом. Например, если метод изменили так, что он начинает обращаться к базе данных вместо файловой системы, то он может сам ловить SQLException
и выбрасывать вместо него вновь создаваемый IOException
, указывая в качестве причины исходное исключение. Обычно рекомендуется изначально объявлять именно те исключения, которые придётся обрабатывать вызывающему коду. Скажем, если метод извлекает входные данные, то для него целесообразно объявить IOException
, а если он оперирует SQL-запросами, то, вне зависимости от природы ошибки, он должен объявлять SQLException
. В любом случае, набор выбрасываемых методом исключений должен тщательно продумываться. При необходимости имеет смысл создавать собственные классы исключений, наследуя их от Exception или других подходящих проверяемых исключений.
Исключения, не требующие проверки
Невозможно сделать проверяемыми вообще все исключения, так как некоторые исключительные ситуации по своей природе таковы, что их возникновение возможно в любом или почти любом месте программы, а предотвратить их программист не в состоянии. При этом бессмысленно указывать подобные исключения в описании функции, так как это пришлось бы сделать для каждой функции, не делая программу понятнее. В основном это исключения, относящиеся к одному из двух видов:
- Исключения, представляющие собой серьёзные ошибки, которые, «по идее», возникать не должны, и которые в обычных условиях не следует обрабатывать программой. Такие ошибки могут возникать как во внешней относительно программы среде, так и внутри неё. Примером такой ситуации может быть ошибка среды исполнения программы на Java. Она потенциально возможна при исполнении любой команды; за редчайшими исключениями в прикладной программе не может быть осмысленного обработчика подобной ошибки — ведь если среда исполнения работает неверно, на что указывает сам факт исключения, нет никакой гарантии, что и обработчик будет исполнен правильно.
- Исключения времени выполнения, обычно связанные с ошибками программиста. Такие исключения возникают из-за логических ошибок разработчика или недостаточности проверок в коде. Например, ошибка обращения по неинициализированному (нулевому) указателю, как правило, означает, что программист либо пропустил где-то инициализацию переменной, либо при выделении динамической памяти не проверил, действительно ли память была выделена. Как первое, так и второе требует исправления кода программы, а не создания обработчиков.
Выносить подобные ошибки вообще за пределы системы обработки исключений нелогично и неудобно, хотя бы потому, что иногда они всё-таки перехватываются и обрабатываются. Поэтому в системах с проверяемыми исключениями часть типов исключений выводится из-под механизма проверки и работает традиционным образом. В Java это классы исключений, унаследованные от java.lang.Error — фатальные ошибки и java.lang.RuntimeException — ошибки времени выполнения, как правило, связанные с ошибками кодирования или недостаточностью проверок в коде программы (неверный аргумент, обращение по пустой ссылке, выход за границы массива, неверное состояние монитора и т. п.).
Грань между «исправимой» и «фатальной» ошибкой очень условная. Например, ошибка ввода-вывода в настольной программе, как правило, «исправимая», и можно сообщить пользователю об этом и продолжить исполнение программы. В веб-скрипте она же «фатальная» — если она случилась, со средой исполнения что-то нехорошее и нужно остановиться, выведя сообщение.