4.06. Ошибки, исключения и отказоустойчивость
Ошибки, исключения и отказоустойчивость
Что такое ошибка
Ошибка — это состояние программы, при котором выполнение не может продолжаться в штатном режиме из-за нарушения ожидаемых условий.
Ошибки возникают по разным причинам:
- нарушение бизнес-логики
- недоступность внешних ресурсов
- неверные входные данные
- сбои в работе оборудования
- ограничения системы
Все ошибки можно разделить на категории:
Исключения — как они работают под капотом
Исключение — это объект, который передаёт информацию об ошибке через стек вызовов до тех пор, пока не будет обработан.
Механизм работы исключений основан на раскрутке стека (stack unwinding):
- При возникновении ошибки создаётся объект исключения
- Стек вызовов разматывается в обратном порядке
- На каждом уровне проверяется наличие блока
catch - При нахождении подходящего обработчика выполнение передаётся в него
- После обработки программа продолжает работу
Раскрутка стека гарантирует корректное освобождение ресурсов:
public void ProcessFile(string path)
{
FileStream file = null;
try
{
file = new FileStream(path, FileMode.Open);
// Работа с файлом
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"Файл не найден: {ex.Message}");
}
finally
{
file?.Close(); // Выполнится всегда, даже при исключении
}
}
В этом примере:
FileStream— объект для работы с файломtry— блок кода, где может возникнуть исключениеcatch— обработчик конкретного типа исключенияfinally— блок, выполняющийся в любом случае
def process_file(path):
file = None
try:
file = open(path, 'r')
# Работа с файлом
except FileNotFoundError as ex:
print(f"Файл не найден: {ex}")
finally:
if file:
file.close() # Выполнится всегда
Когда использовать исключения, а когда — коды ошибок
Выбор механизма обработки ошибок зависит от контекста и языка программирования.
Исключения подходят для:
- критических ошибок, требующих немедленного внимания
- ситуаций, которые не должны происходить в нормальной работе
- разделения бизнес-логики от обработки ошибок
- языков с поддержкой исключений (C#, Java, Python, C++)
Коды ошибок предпочтительны для:
- ожидаемых ситуаций, являющихся частью нормального потока
- системного программирования и низкоуровневых операций
- языков без исключений (C, Go, Rust)
- случаев, где важна производительность
Пример использования кодов ошибок в Go:
func readFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err // Возвращаем ошибку явно
}
return data, nil
}
// Использование
content, err := readFile("config.txt")
if err != nil {
log.Printf("Ошибка чтения: %v", err)
return
}
// Продолжаем работу
Пример с исключениями в C#:
public string ReadFile(string path)
{
try
{
return File.ReadAllText(path);
}
catch (FileNotFoundException)
{
// Обработка конкретной ошибки
return "Файл не найден";
}
catch (IOException ex)
{
// Обработка других ошибок ввода-вывода
throw new ApplicationException("Ошибка чтения файла", ex);
}
}
Неуправляемые исключения и их последствия
Неуправляемое исключение — это исключение, которое не было перехвачено ни одним блоком catch в стеке вызовов.
Последствия неуправляемых исключений:
- аварийное завершение программы
- потеря несохранённых данных
- повреждение состояния системы
- плохой пользовательский опыт
В разных средах выполнения неуправляемые исключения ведут себя по-разному:
// C# - приложение завершается
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
Console.WriteLine($"Необработанное исключение: {e.ExceptionObject}");
// Логирование перед завершением
};
# Python - выводится трассировка и завершение
import sys
def handle_exception(exc_type, exc_value, exc_traceback):
print(f"Критическая ошибка: {exc_value}")
# Логирование
sys.excepthook = handle_exception
// JavaScript в браузере - ошибка в консоли, скрипт останавливается
window.addEventListener('error', (event) => {
console.error('Глобальная ошибка:', event.error);
event.preventDefault(); // Предотвращаем стандартное поведение
});
Логирование ошибок — что, когда и зачем записывать
Логирование — это запись информации о событиях, происходящих в программе, для последующего анализа.
Что нужно логировать:
| Уровень | Когда использовать | Пример |
|---|---|---|
TRACE | Подробная отладочная информация | Вход и выход из методов |
DEBUG | Информация для разработчиков | Значения переменных, SQL-запросы |
INFO | Стандартные операции | Запуск приложения, успешная обработка |
WARN | Потенциальные проблемы | Устаревшие методы, медленные операции |
ERROR | Ошибки, требующие внимания | Исключения, сбои операций |
FATAL | Критические ошибки | Неуправляемые исключения, аварийное завершение |
Пример структурированного логирования:
using Microsoft.Extensions.Logging;
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public async Task ProcessOrder(Order order)
{
_logger.LogInformation("Начало обработки заказа {OrderId}", order.Id);
try
{
// Логируем входные данные
_logger.LogDebug("Данные заказа: {@Order}", order);
await ValidateOrder(order);
await SaveOrder(order);
_logger.LogInformation("Заказ {OrderId} успешно обработан", order.Id);
}
catch (ValidationException ex)
{
_logger.LogWarning(ex, "Ошибка валидации заказа {OrderId}", order.Id);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Критическая ошибка при обработке заказа {OrderId}", order.Id);
throw;
}
}
}
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def process_order(order):
logger.info(f"Начало обработки заказа {order.id}")
try:
logger.debug(f"Данные заказа: {order}")
validate_order(order)
save_order(order)
logger.info(f"Заказ {order.id} успешно обработан")
except ValidationError as ex:
logger.warning(f"Ошибка валидации заказа {order.id}: {ex}")
raise
except Exception as ex:
logger.error(f"Критическая ошибка при обработке заказа {order.id}", exc_info=True)
raise
@Order в C#). Это позволяет эффективно искать и анализировать логи с помощью инструментов вроде Elasticsearch, Splunk или Seq.Игнорирование ошибок
Игнорирование ошибок — это практика, при которой возникшие ошибки не обрабатываются и не логируются.
Последствия игнорирования ошибок:
- скрытые проблемы в коде
- непредсказуемое поведение программы
- сложность диагностики проблем
- деградация качества системы со временем
Примеры плохой практики:
// Плохо: пустой catch
try
{
ProcessData();
}
catch
{
// Ничего не делаем
}
// Плохо: игнорирование результата
int.TryParse("invalid", out int result); // result = 0, но мы не знаем об ошибке
# Плохо: подавление всех исключений
try:
process_data()
except:
pass # Молча игнорируем любую ошибку
# Плохо: игнорирование возвращаемого значения
result = some_function() # Результат не используется
// Плохо: игнорирование ошибки
data, _ := readFile("config.txt") // Ошибка проигнорирована
Правильный подход — всегда обрабатывать ошибки осмысленно:
try
{
ProcessData();
}
catch (SpecificException ex)
{
logger.LogWarning("Не критичная ошибка, продолжаем работу: {Message}", ex.Message);
// Продолжаем работу с дефолтными значениями
}
Когда можно игнорировать ошибки
Иногда игнорирование ошибок допустимо, но только в обоснованных случаях:
- попытка удаления несуществующего файла
- проверка существования ресурса перед созданием
- обработка временных сетевых сбоев с повторными попытками
- graceful degradation при недоступности не критичных компонентов
Принудительные действия — форсинг вызовов, игнорирование валидаций
Принудительные действия — это операции, которые обходят стандартные механизмы проверки и валидации.
Типичные сценарии принудительных действий:
- обход валидации данных
- принудительное выполнение операций
- отключение проверок безопасности
- форсированные обновления
Примеры реализации:
public class OrderService
{
public void CreateOrder(Order order, bool force = false)
{
if (!force)
{
ValidateOrder(order);
}
// Принудительное создание без валидации
SaveOrder(order);
}
private void ValidateOrder(Order order)
{
if (order.Amount <= 0)
throw new ValidationException("Сумма заказа должна быть положительной");
if (string.IsNullOrWhiteSpace(order.CustomerName))
throw new ValidationException("Имя клиента обязательно");
}
}
def create_order(order, force=False):
if not force:
validate_order(order)
# Принудительное создание
save_order(order)
def validate_order(order):
if order.amount <= 0:
raise ValueError("Сумма заказа должна быть положительной")
if not order.customer_name:
raise ValueError("Имя клиента обязательно")
Риски принудительных действий:
- нарушение целостности данных
- обход бизнес-правил
- сложность отладки
- потенциальные уязвимости безопасности
Рекомендации по использованию
Принудительные действия должны:
- быть явно обозначены в коде и документации
- логироваться с указанием причины
- использоваться только в крайних случаях
- иметь ограничения по правам доступа
- сопровождаться комментариями с обоснованием