Перейти к основному содержимому

Ошибки, исключения и отказоустойчивость

Разработчику Архитектору Инженеру

Ошибки, исключения и отказоустойчивость

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

Маршрут по статье

Сначала — определения: что такое ошибка, почему она возникает, чем ошибка отличается от исключения.
Затем — механизмы в коде (стек, try/catch, коды возврата, логи).
Для сервисов под нагрузкойотказоустойчивость (retry, circuit breaker, DLQ) в отдельных главах по архитектуре.
Языковые статьи (Java, Python, JS…) предполагают, что эта база уже прочитана — см. перекрёстные ссылки.


Что такое ошибка

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

Ошибка (в узком смысле для прикладного разработчика) — критический, фатальный сбой на уровне системного окружения, инфраструктуры или среды исполнения (виртуальной машины, процесса ОС), после которого программа не может безопасно продолжать работу в текущем процессе. Примеры — исчерпание памяти на уровне JVM (OutOfMemoryError), внутренний сбой VM, аварийное завершение процесса из‑за сегментации, необработанный фатальный сигнал.

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

УровеньПримерПроцесс после сбоя
Фатальная ошибка средыOutOfMemoryError, crash, SIGSEGVОбычно завершение или перезапуск
Ошибка сценария / бизнеса"Корзина пуста", неверный ИННПродолжает работу, ответ клиенту
Ошибка интеграцииHTTP 503, таймаут БДЗависит от политики (retry, fallback)

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


Почему ошибки возникают

Сбои не сводятся к "плохому коду". Источники пересекаются:

ИсточникПримеры
Логика и дизайнНеверная формула, гонка данных, отсутствие проверки границ
Ввод и контрактБитый JSON, устаревший клиент, нарушение API
Внешние ресурсыФайл не найден, DNS, недоступность партнёрского API
ИнфраструктураПерегрузка CPU/RAM, диск, сеть, лимиты облака
Среда исполненияJVM, GC, драйверы, несовместимая версия ОС
ИнструментыДефект компилятора, битая сборка (реже)

На этапе разработки часть проблем отсекают компилятор (синтаксис) и статический анализ (warnings). На этапе выполнения остаются семантические сбои, интеграции и редкие "плавающие" баги — их ловят тестами, мониторингом и логами.

Источники дефектов локализуют в тестировании и отладке; часть доходит до пользователей и попадает в телеметрию (Windows Error Reporting, Breakpad, аналоги).


Чем отличается ошибка от исключения

Два термина часто путают, потому что в одном предложении звучат как синонимы. В учебниках и в API языков у них разные роли.

Исключение — это предсказуемое, ожидаемое отклонение от нормального сценария работы, которое программа может и должна обработать, чтобы продолжить выполнение (или корректно завершить один запрос, не роняя весь сервис). Технически в языках с try/catch это объект (или значение), который передаётся вверх по стеку до подходящего обработчика — файл не найден, неверный аргумент, таймаут одной операции.

Ошибка (в паре "ошибка / исключение" на уровне JVM и родственных моделей) — критический, фатальный сбой среды или инфраструктуры, после которого продолжать работу в том же процессе нельзя или нецелесообразно — нехватка памяти, внутренний сбой VM, повреждение состояния рантайма. Такие события обычно не предполагают обычного бизнес-catch — их лечат перезапуском, масштабированием, алертами.

Ожидаемый сбой сценария → исключение / Result / код возврата → обработали → продолжили
Фатальный сбой среды → Error / crash / panic (критич.) → процесс/сервис останавливают
ИсключениеОшибка (фатальная)
СмыслОтклонение, заложенное в контрактСбой среды, ломающий процесс
Обработкаtry/catch, except, rescue, ResultЛог, алерт, рестарт, масштабирование
Пример (Java)FileNotFoundException, IllegalArgumentExceptionOutOfMemoryError, StackOverflowError
Пример (Go)Нет исключений; error как значениеpanic при нарушении инварианта (редко)

В Java иерархия Throwable формально делит: Exception (обычно обрабатываемые) и Error (фатальные для VM). В Python есть Exception и BaseException (включая SystemExit, KeyboardInterrupt). В JavaScript нет класса Error vs Exception на уровне языка — все встроенные типы наследуют Error; фатальность определяется контекстом (неперехваченное исключение, unhandledrejection). Подробнее по языкам — в таблице ниже.

Термины в глоссарии и "Обработка исключений" согласованы с этой статьёй.

Способы реагирования в коде можно свести к трём семействам:

Катастрофический сценарий — пользователь ввёл мусор в поле телефона, а сервер упал для всех. Штатная реакция — сообщение об ошибке ввода и отказ только для этого запроса. Значит, дефект в обработке граничного случая, а не "норма".

Для ожидаемых сбоев (битый ввод, нет файла, таймаут API) используют проверки (if, коды возврата, Result) или исключения — заранее описанный "план Б" в catch / except / rescue. Исключительная ситуация — когда дальнейшие вычисления по основному алгоритму бессмысленны (нет файла — нельзя читать строки). Игнорировать такой сбой опасно: программа продолжит с чужими данными.

Стиль кода — Исключения в читаемом коде.


Словарь сбоев — как различать термины

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

ТерминСутьТипичное проявление
Программная ошибка (баг)Дефект в исходном коде или дизайне, дающий неожиданное поведение или неверный результатНеверная сумма, падение только на одном клиенте, "плавающий" сбой
Ошибка времени выполненияНарушение условий при работе уже запущенной программыделение на ноль, обращение к null, ошибка сегментации
ИсключениеОжидаемое отклонение; механизм языка передаёт сигнал вверх по стекуtry / catch, raise, throw
Ошибка (фатальная)Сбой среды/VM, процесс обычно не продолжает работуOutOfMemoryError, crash, panic (критич.)
Аварийный отказ (крэш, crash)Аварийное завершение процесса или ОСДиалог "программа прекратила работу", дамп памяти
ЗависаниеНет реакции на действия пользователя или бесконечный цикл без прогресса"Не отвечает", застывший экран
ПодвисаниеКраткая пауза, после которой работа возобновляется без перезапускаДолгий запрос к сети, тяжёлый расчёт

Программная ошибка (англ. bug, "жучок") — ошибка в программе или в её проектировании, из‑за которой система ведёт себя иначе, чем задумано. Термин обычно относят к дефектам, видимым на этапе работы программы, в отличие от ошибок проектирования на бумаге и от синтаксических ошибок, которые транслятор не пропускает. Отчёт о проблеме в эксплуатации называют отчётом об ошибке (error report); если процесс оборвался аварийно — крэш-репортом (crash report).

Источники дефектов — ошибки разработчика в коде и дизайне, сбои инструментов (компилятор, среда), аппаратура, драйверы, внешние сервисы. Локализуют и устраняют их в тестировании и отладке; часть доходит до пользователей и попадает в телеметрию (Windows Error Reporting, Breakpad, CrashRpt и аналоги).

Классификация по этапу разработки

ЭтапПримерКто обнаруживает
СинтаксическаяПропущена }, несогласованные скобкиКомпилятор, IDE — сборка невозможна
Предупреждение (warning)Неинициализированная переменнаяКомпилятор предупреждает; решение за автором кода
Семантическая / времени выполненияСложили строку с числом вместо умножения, выход за границы массиваТесты, пользователь, мониторинг

По важности для релиза часто выделяют блокирующие, критические (showstopper), серьёзные, незначительные и косметические дефекты. По повторяемости — постоянные, "плавающие" и проявляющиеся только у части клиентов (другая ОС, настройки, нагрузка).

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

Аварийный отказ и зависание

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

Типичные причины краша приложения:

  • чтение или запись чужой памяти (ошибка сегментации, access violation);
  • привилегированная или недопустимая инструкция;
  • неверные аргументы системного вызова;
  • деление на ноль, NaN в арифметике с плавающей точкой (зависит от архитектуры).

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

В системах с вытесняющей многозадачностью (современные Windows, Linux, macOS) зависший поток одного приложения не всегда останавливает всю ОС — планировщик переключается на другие задачи, но "залипший" поток всё ещё может съедать CPU или держать блокировку (файл, мьютекс) и подвешивать соседние части системы. В кооперативной многозадачности один поток без отдачи управления блокирует всех.

Причины зависания со стороны программы — бесконечный цикл, взаимная блокировка (deadlock), ожидание сети или диска без таймаута, нехватка памяти. Со стороны железа — перегрев, сбой RAM, повреждённый диск. Иногда кажется, что "всё зависло", хотя идёт очень долгая операция — стоит подождать или смотреть индикатор нагрузки, прежде чем убивать процесс.


Исключительные ситуации — синхронные и асинхронные

ТипКогда возникаетПримеры
СинхронныеВ известных точках кода, при выполнении конкретной операцииread, malloc, деление на ноль
АсинхронныеВ любой момент, не привязаны к текущей строкесигнал ОС, обрыв питания, приход данных по прерыванию

Структурная обработка — блок try с ветками catch и (часто) finally / ensure: язык сам связывает обработчик с участком кода. Неструктурная — регистрация обработчика на тип события (как в старых диалектах BASIC с ON ERROR GOTO); для асинхронных сигналов она ещё встречается, для обычного прикладного кода неудобна.

Два режима после обработчика:

  • С возвратом — проблема устранена, выполнение продолжается в той же точке (типично для асинхронных событий);
  • Без возврата — после catch управление переходит в заранее заданное место; команда, вызвавшая сбой, фактически заменяется переходом (обычный случай для try / catch в Java, C#, Python).

Блок с гарантированным завершением (finally, ensure, деструкторы в C++ через RAII) не "лечит" исключение, а обязательно выполняет очистку (закрыть файл, откатить транзакцию) перед дальнейшей раскруткой стека.

Без перехвата в приложении срабатывает системный обработчик — сообщение пользователю, запись отчёта об ошибке (тип сбоя, стек, версия программы и ОС, иногда минидамп) и отправка разработчику или в централизованную базу (как Windows Error Reporting, Breakpad в Firefox, Apport в Ubuntu).

Ожидаемый сбой оформляют так:

попробовать:
вывод("Привет! Это нормальное выполнение программы.")
исключение:
вывод("Ой! Что-то пошло не так...)

Исключения — как они работают под капотом

Исключение — это объект, который передаёт информацию об ошибке через стек вызовов до тех пор, пока не будет обработан.

Механизм работы исключений основан на раскрутке стека (stack unwinding):

  1. При возникновении ошибки создаётся объект исключения
  2. Стек вызовов разматывается в обратном порядке
  3. На каждом уровне проверяется наличие блока catch
  4. При нахождении подходящего обработчика выполнение передаётся в него
  5. После обработки программа продолжает работу

Play ITЗагрузка интерактивного демо…

АЛГОРИТМ ОБРАБОТАТЬ_ФАЙЛ(путь)
файл := пусто
попробовать
файл := открыть(путь)
// работа с файлом
поймать ОшибкаФайлНеНайден как ошибка
вывести("Файл не найден:", ошибка.сообщение)
в_любом_случае
если файл ≠ пусто то
закрыть(файл) // выполнится даже при исключении
конец
конец
КОНЕЦ

АЛГОРИТМ РАСКРУТКА_СТЕКА(исключение)
пока стек_вызовов не пуст
фрейм := снять_верхний_фрейм(стек_вызовов)
если в фрейме есть обработчик для типа исключение то
выполнить(обработчик)
вернуть
иначе
выполнить_блоки_очистки_фрейма(фрейм) // finally / деструкторы
конец
конец
аварийно_завершить_программу(исключение)
КОНЕЦ
ШагСмысл
в_любом_случаеАналог finally — ресурс закрывается при любом исходе попробовать
РАСКРУТКА_СТЕКАПоиск catch снизу вверх; без обработчика — падение программы

Раскрутка стека гарантирует корректное освобождение ресурсов.

Справочно на C#

Код ITЗагрузка примера кода…

В этом примере:

  • FileStream — объект для работы с файлом
  • try — блок кода, где может возникнуть исключение
  • catch — обработчик конкретного типа исключения
  • finally — блок, выполняющийся в любом случае

Справочно на Python

Код ITЗагрузка примера кода…


Когда использовать исключения, а когда — коды ошибок

Выбор механизма обработки ошибок зависит от контекста и языка программирования.

Короткая практическая эвристика:

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

Исключения подходят для:

  • критических ошибок, требующих немедленного внимания
  • ситуаций, которые не должны происходить в нормальной работе
  • разделения бизнес-логики от обработки ошибок
  • языков с поддержкой исключений (C#, Java, Python, C++)

Коды ошибок предпочтительны для:

  • ожидаемых ситуаций, являющихся частью нормального потока
  • системного программирования и низкоуровневых операций
  • языков без исключений (C, Go, Rust)
  • случаев, где важна производительность

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

АЛГОРИТМ ПрочитатьФайл(путь)
попробовать
содержимое := прочитать_байты(путь)
вернуть (содержимое, нет_ошибки)
поймать любая_ошибка как e
вернуть (пусто, e)
конец
КОНЕЦ

(данные, ошибка) := ПрочитатьФайл("config.txt")
если ошибка ≠ нет_ошибки то
записать_в_лог(ошибка)
вернуть
конец
// основная логика с данными

Справочно на Go

Код ITЗагрузка примера кода…

Цепочка причин и повторный проброс

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

ЯзыкСохранить causeПробросить дальше
Javanew ServiceException("…", e)throw без catch или после логирования
C#throw new AppException("…", ex)throw; в catch (не throw ex)
Pythonraise DomainError("…") from eraise без аргументов в except
JavaScriptnew Error("…", { cause: e })throw после логирования

Антипаттерн: пустой catch или новое исключение без ссылки на предыдущее — см. типичные ошибки ниже.

Пример с исключениями в C#:

Код ITЗагрузка примера кода…


Неуправляемые исключения и их последствия

Неуправляемое исключение — это исключение, которое не было перехвачено ни одним блоком catch в стеке вызовов.

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

  • аварийное завершение программы
  • потеря несохранённых данных
  • повреждение состояния системы
  • плохой пользовательский опыт

В разных средах выполнения неуправляемые исключения ведут себя по-разному:

// C# - приложение завершается
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
Console.WriteLine($"Необработанное исключение: {e.ExceptionObject}");
// Логирование перед завершением
};

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Подробнее по JS и Node — Обработка исключений в JavaScript, асинхронность и rejection.

Рекомендация

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


Логирование ошибок — что, когда и зачем записывать

Логирование — это запись информации о событиях, происходящих в программе, для последующего анализа.

Полезная практика для продакшена — корреляция логов:

  • добавляйте traceId и requestId в каждый лог;
  • фиксируйте ключевые параметры операции (например, orderId, userId, tenantId);
  • храните причину и тип ошибки отдельно от пользовательского сообщения.

Так инженер поддержки быстрее собирает цепочку событий без ручного поиска по разным сервисам.

Что нужно логировать:

УровеньКогда использоватьПример
TRACEПодробная отладочная информацияВход и выход из методов
DEBUGИнформация для разработчиковЗначения переменных, SQL-запросы
INFOСтандартные операцииЗапуск приложения, успешная обработка
WARNПотенциальные проблемыУстаревшие методы, медленные операции
ERRORОшибки, требующие вниманияИсключения, сбои операций
FATALКритические ошибкиНеуправляемые исключения, аварийное завершение

Пример структурированного логирования:

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Структурированное логирование

Используйте структурированные логи с контекстными данными (например, @Order в C#). Это позволяет эффективно искать и анализировать логи с помощью инструментов вроде Elasticsearch, Splunk или Seq.


Игнорирование ошибок

Игнорирование ошибок — это практика, при которой возникшие ошибки не обрабатываются и не логируются.

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

Последствия игнорирования ошибок:

  • скрытые проблемы в коде
  • непредсказуемое поведение программы
  • сложность диагностики проблем
  • деградация качества системы со временем

Примеры плохой практики:

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

// Плохо: игнорирование ошибки
Данные, _ := readFile("config.txt") // Ошибка проигнорирована

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

Код ITЗагрузка примера кода…

Когда можно игнорировать ошибки

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

  • попытка удаления несуществующего файла
  • проверка существования ресурса перед созданием
  • обработка временных сетевых сбоев с повторными попытками
  • graceful degradation при недоступности не критичных компонентов

Принудительные действия — форсинг вызовов, игнорирование валидаций

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


Отказоустойчивость распределённых систем

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

МеханизмЗачем
ТаймаутыНе держать потоки бесконечно на мёртвой зависимости
Retry с backoffПовторить транзиентный сбой, не усугубляя перегрузку
Circuit breakerВременно не звонить в падающий сервис
Fallback / graceful degradationУрезанный, но рабочий ответ
DLQУбрать сообщение, которое ломает воркер, на ручной разбор
ИдемпотентностьБезопасные повторы при доставке "как минимум один раз"

Углубление:

На границе одного процесса по-прежнему действуют исключения, коды error и логирование из разделов выше; на границе сети — контракт HTTP, коды статуса и политики повторов.


Справочник и первоисточники

Краткие формулировки в этой статье согласованы с разделами русскоязычной Википедии (можно использовать как внешний словарь терминов):

Для школьного уровня — Ошибки и сбои в программах в курсе "Базовая информатика".


Связанные материалы энциклопедии

Проектирование и архитектура (раздел 7):

Языковые главы (после этой теории)

Перед разделами "Обработка исключений" в курсах языков имеет смысл пройти определения выше (ошибка vs исключение).

ЯзыкОбработкаИерархия / типыБез исключений
JavaОбработка исключений в JavaИерархия классов исключений в Java
PythonОбработка исключений в PythonРаспространённые типы исключений
C#Обработка исключений в C#Иерархия классов исключений в C#
JavaScriptОбработка исключений в JavaScriptВстроенные типы ошибок и их обработка
TypeScriptОбработка ошибок в TypeScriptСправочник по TypeScriptResult / union
C++Обработка исключений в C++Иерархия исключений в стандартной библиотеке C++коды + RAII
PHPОбработка исключений в прикладном коде PHPИерархия исключений в PHP
Kotlin (JVM)21 JavaИерархия исключений в KotlinResult в API
RubyИерархия исключений в Rubyтам же
GoОбработка ошибок в Goerror, panic
RustОбработка ошибок в RustResult, panic!
SwiftОбработка ошибок в Swiftthrows / Result

Что запомнить

  • Исключение — ожидаемое отклонение, которое обрабатывают, чтобы продолжить сценарий (или корректно завершить один запрос).
  • Ошибка (фатальная) — сбой среды/VM/процесса, после которого обычно нужен рестарт или эскалация, а не "тихий" catch.
  • Баг, крэш, зависание — не синонимы; путаница мешает приоритетам и тексту для пользователя.
  • Ожидаемые бизнес-сбои — коды, Result, валидация; редкие нарушения инварианта — исключения.
  • Сохраняйте cause при пробросе; finally / defer / RAII закрывают ресурсы при любом исходе.
  • Отказоустойчивость сервиса — таймауты, retry, breaker, DLQ — отдельный блок.

Типичные ошибки

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

Мини-практика

  1. Возьмите один endpoint и выпишите все возможные ошибки.
  2. Разделите их на ожидаемые и критичные.
  3. Для каждой ошибки задайте код ответа, уровень логирования и действие клиента.
  4. Проверьте, что в лог попадают traceId, ключевые параметры и исходное исключение.

Типичные сценарии принудительных действий:

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

Примеры реализации:

Код ITЗагрузка примера кода…

Код ITЗагрузка примера кода…

Риски принудительных действий:

  • нарушение целостности данных
  • обход бизнес-правил
  • сложность отладки
  • потенциальные уязвимости безопасности
Рекомендации по использованию

Принудительные действия должны:

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