10 шокирующих ошибок в обработке ошибок, которые могут разрушить ваш код

28 декабря 2025 г.

Вступление

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

Японское хокку, отражающее суть проблемы:

Тихий код спит,
Но неинициализированный
Сон – буря в строке.

Пересказ Reddit‑поста своими словами

Автор оригинального поста (пользователь nekokattt) привёл в ответ на запрос JSON‑объект:


{ "error": false, "succeeded": true }

На первый взгляд всё выглядит корректно: поле error – логическое, succeeded – тоже логическое. Однако в комментариях быстро выяснилось, что за этой «простой» структурой скрывается более глубокая проблема.

Пользователь teerre отметил, что подобные конструкции часто приводят к «footguns» – ситуациям, когда небольшая оплошность в коде приводит к тяжёлым последствиям. Он советует всегда инициализировать переменные, чтобы избежать скрытых багов.

Комментарий Kered13 дал лаконичное TL;DR: «Тип был простым struct без конструктора по умолчанию, а переменная не была инициализирована». Это объясняет, почему в рантайме получались случайные значения.

Другие участники, такие как mpanase и Derpicide, обсуждали альтернативные схемы представления ошибок: вместо двух булевых полей предлагали использовать более выразительные типы ({ "error": string, "succeeded": bool }) или комбинировать статус операции и сообщение об ошибке.

Суть проблемы, хакерский подход и основные тенденции

Ключевая ошибка – отсутствие инициализации переменной, когда тип данных представляет собой структуру без конструктора по умолчанию. В языках типа C++ или Rust такой код компилируется, но переменная получает «мусорные» значения, что в дальнейшем приводит к неверным проверкам if (error) или if (succeeded).

Хакерский подход к решению:

  • Всегда задавать начальные значения полям структуры.
  • Использовать типы, которые явно указывают на наличие ошибки (например, Result<T, E> в Rust).
  • Применять статический анализ кода, который выявит неинициализированные переменные.

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

Детальный разбор проблемы с разных сторон

1. Техническая сторона

В языках без автоматической инициализации (C, C++, Rust без #![allow(uninitialized)]) переменная, объявленная как:


struct OperationResult {
    bool error;
    bool succeeded;
};
OperationResult result; // без инициализации

получит произвольные битовые паттерны. При проверке if (result.error) условие может сработать даже если операция прошла успешно, что приводит к ложным срабатываниям.

2. Психологическая сторона

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

3. Организационная сторона

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

Практические примеры и кейсы

Рассмотрим два сценария: один с ошибкой, второй – с правильным решением.

Сценарий A: Неинициализированный struct


struct ApiResponse {
    bool error;
    bool succeeded;
};

ApiResponse call_api() {
    ApiResponse resp; // НЕ инициализировано
    // ... код, который может установить только один из флагов
    return resp;
}

int main() {
    auto r = call_api();
    if (r.error) {
        std::cout << "Ошибка!\n";
    } else if (r.succeeded) {
        std::cout << "Успех!\n";
    }
}

В этом примере, если функция call_api не установит ни error, ни succeeded, то в main произойдёт случайный вывод.

Сценарий B: Явная инициализация и тип Result


struct ApiResponse {
    bool error = false;      // Инициализация по умолчанию
    bool succeeded = false;  // Инициализация по умолчанию
};

ApiResponse call_api() {
    ApiResponse resp;
    // Предположим, запрос прошёл успешно
    resp.succeeded = true;
    return resp;
}

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

Экспертные мнения из комментариев

«I know you're being funny, and yeah there is probably a better way to do this, but I’ve built processes before and an operation could be unsuccessful without there being an error, or successful with errors encountered. Having both separate might be useful to the caller in some way.» – Derpicide

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

«That's why I teach to value initialize everything. Way less footguns» – teerre

teerre советует «инициализировать всё», тем самым минимизируя риск «подводных камней».

«The type was a simple struct with no default constructor and the variable was not initialized.» – Kered13

Kered13 дал лаконичную техническую причину проблемы.

Возможные решения и рекомендации

  1. Инициализация по умолчанию. В любой структуре задавайте начальные значения полей.
  2. Явные типы результата. Используйте конструкции вроде Result<T, E> (Rust), Either<L, R> (Scala) или Option<T> (C#) вместо простых булевых флагов.
  3. Статический анализ. Интегрируйте инструменты clang-tidy, SonarQube или mypy (для Python) в CI, чтобы ловить неинициализированные переменные.
  4. Тестовое покрытие. Пишите юнит‑тесты, проверяющие каждый путь: успех без ошибки, ошибка без успеха, оба флага одновременно.
  5. Код‑ревью. Включайте в чек‑лист пункт «проверить инициализацию всех переменных».

Прогноз развития ситуации

С ростом популярности языков с безопасной типизацией (Rust, Kotlin, Swift) тенденция к использованию «богатых» типов результата будет усиливаться. Однако в корпоративных проектах, где доминируют C/C++ и C#, проблема неинициализированных переменных останется актуальной до тех пор, пока не будет внедрена культура строгой инициализации и автоматизированного анализа кода.

Практический пример на Python

Ниже – полностью рабочий пример, демонстрирующий правильную инициализацию и обработку ошибок в стиле «Result». Мы создаём функцию process_data, которая возвращает словарь с полями error (строка или None) и succeeded (булево). Внутри функции гарантируем, что оба поля всегда присутствуют, а любые исключения логируются.


# -*- coding: utf-8 -*-
import logging
from typing import Any, Dict, Optional

# Настраиваем базовый логгер
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    datefmt='%H:%M:%S'
)

def process_data(data: Optional[Any]) -> Dict[str, Any]:
    """
    Обрабатывает входные данные и возвращает результат в виде словаря.
    
    Возвращаемый словарь всегда содержит два ключа:
        - 'error'      : строка с описанием ошибки или None, если ошибки нет
        - 'succeeded'  : булево значение, указывающее на успешность операции
    
    Args:
        data: Входные данные, могут быть None или любой объект.
    
    Returns:
        dict: Результат обработки.
    """
    # Явно инициализируем результат, чтобы гарантировать наличие всех полей
    result: Dict[str, Any] = {
        "error": None,      # тип: Optional[str]
        "succeeded": False  # тип: bool
    }

    try:
        # Пример простой проверки: данные должны быть непустыми
        if data is None:
            # Ошибка бизнес‑логики – отсутствие данных
            result["error"] = "Получены пустые данные"
            logging.warning("process_data: %s", result["error"])
        else:
            # Здесь могла бы быть сложная бизнес‑логика
            # Для демонстрации считаем, что всё прошло успешно
            result["succeeded"] = True
            logging.info("process_data: операция завершена успешно")
    except Exception as exc:
        # Любое неожиданное исключение попадает сюда
        result["error"] = str(exc)
        logging.error("process_data: непредвиденная ошибка – %s", exc)

    return result

# ------------------- Тестовый блок -------------------
if __name__ == "__main__":
    # Сценарий 1: передаём корректные данные
    good_data = {"id": 123, "value": "test"}
    res_good = process_data(good_data)
    print("Сценарий 1 →", res_good)

    # Сценарий 2: передаём None (ошибка бизнес‑логики)
    res_none = process_data(None)
    print("Сценарий 2 →", res_none)

    # Сценарий 3: искусственно вызываем исключение
    def faulty_function():
        raise RuntimeError("Симуляция сбоя")

    try:
        faulty_function()
    except RuntimeError as e:
        # Прямо передаём ошибку в process_data, имитируя сбой внутри
        res_error = process_data(e)
        print("Сценарий 3 →", res_error)

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

  • Переменная result инициализируется сразу же после создания, что исключает «мусорные» значения.
  • Любая ошибка (как бизнес‑логика, так и исключения) фиксируется в поле error, а статус успеха хранится в succeeded.
  • Логирование позволяет отследить, где именно произошёл сбой, без необходимости бросать исключения наружу.

Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE