Логотип ConstructiCat
CodeBust.
Навигация по разделу ▾

Код только счастливого пути.

ИИ-ассистенты склонны генерировать код, который обрабатывает только успешный, корректный случай — пропуская валидацию ввода, обработку ошибок, проверки на null/пустоту и краевые случаи — поэтому код работает в демо и ломается в продакшене.

##Признаки и симптомы

Ревьюер замечает код только счастливого пути, когда каждая строка предполагает, что предыдущая завершилась успехом: сетевые вызовы не проверяются на статус не-2xx, JSON.parse/await res.json() никогда не оборачиваются, nullable-поля разыменовываются напрямую, к массивам обращаются по индексу без проверки длины, а внешний ввод течёт прямо в логику без валидации. Обычно есть ровно один путь возврата и нет throw, нет охранных условий и нет catch (или catch, который пуст или содержит лишь console.log).

// Сгенерировано ИИ: существует только сценарий успеха
async function getUserCity(userId) {
  const res = await fetch(`/api/users/${userId}`);
  const user = await res.json();
  return user.address.city.toUpperCase();
}

Режимы сбоев молча отсутствуют: res.ok никогда не проверяется (404/500 возвращает тело ошибки, на котором .json() может бросить исключение), user может быть {}, address может быть null, city может быть undefined → Cannot read properties of undefined. Функция "работает" против счастливой заглушки и бросает исключение против реальности.

Характерные признаки в диффе:

  • Новая async-функция без try/catch и без .catch() на промисе.
  • Прямые цепочки свойств (a.b.c.d) на данных, пересёкших границу доверия/ввода-вывода.
  • Непроверенные arr[0], find(...)!, приведения as или утверждения non-null (!), подменяющие настоящую обработку.
  • // TODO: handle errors или голый catch (e) {}, оставленный как заглушка.
  • Описание PR гласит "обрабатывает X", но реализована только ветка, где X завершается успехом.

##Причины проблемы

Почему модели это порождают

  • Вероятность следующего токена благоволит каноническому потоку. Наиболее вероятное продолжение после const user = await res.json() — это return user.something, а не проверка статуса. Обработка ошибок — это высоковариативный шаблонный код, различающийся от базы к базе, поэтому статистически он "неожиданный" и отбрасывается.
  • Обучающие данные смещены к счастливому пути. Туториалы, сниппеты README, посты блогов и принятые ответы Stack Overflow вырезают валидацию и обработку ошибок ради краткости ("обработка ошибок опущена для ясности"). Модель училась на тексте, который намеренно убрал ровно тот код, который вам нужен.
  • Подхалимство / награда за опрятный вид. Настроенные через RLHF ассистенты вознаграждаются за лаконичные, прямо отвечающие ответы. Защитный код выглядит как шум, поэтому модель оптимизирует аккуратный сниппет, который кажется ответом на промпт.
  • Нет контекста репозитория. Модель не знает, что у вас есть тип AppError, обёртка Result<T>, схема zod или соглашение о логировании, поэтому не может их переиспользовать — и по умолчанию выбирает "ничего", вместо того чтобы угадывать ваши соглашения.
  • Задокументированные поведенческие тенденции. Анализ 300+ репозиториев от OX Security называет среди главных ИИ-антипаттернов избегание рефакторинга — ИИ «отлично генерирует новый код, но никогда не возвращается, чтобы улучшить архитектуру» — и фиксацию на учебнике (отчёт OX): модель выдаёт рабочий код под непосредственный промпт и не возвращается, чтобы сделать его надёжнее. Кроме того, сгенерированный LLM код на Java содержал в среднем на 73% больше запахов реализации, чем эталонные решения людей, причём сильнее всего выросли незавершённость (Incompleteness), несогласованное именование (Inconsistent Naming Convention) и избыточность (Redundancy) (arXiv 2510.03029).

Почему это вредит

  • Корректность: падения на null, пустых коллекциях, тайм-аутах и ответах с кодом не 200 — ровно те входные данные, которые не появляются в быстром ручном тесте.
  • Безопасность: счастливый путь неявно доверяет своему вводу. Пропущенная валидация на границах — это то, как проникают ошибки внедрения, обхода пути и загрязнения прототипа. В отчёте OX есть выражение «insecure by dumbness» («небезопасно по глупости») — так названы приложения, которые люди без технической подготовки собирают с помощью ИИ и выкатывают без экспертизы в безопасности; именно там непроверенные счастливые пути уходят в продакшен без ревью (OX Security).
  • Нагрузка на ревью и отладку: в опросе разработчиков Stack Overflow 2025 года 66% разработчиков называют "ИИ-решения, которые почти верны, но не совсем" своим главным разочарованием, а 45% говорят, что отладка сгенерированного ИИ кода более трудозатратна. Код счастливого пути — архетип "почти верного": читается нормально и падает во время выполнения.
  • Накопление техдолга: поскольку модель не рефакторит и не переиспользует существующие утилиты для ошибок, каждая функция счастливого пути — это свежий одноразовый ком. Данные GitClear 2025 года показывают более широкую картину (GitClear 2025: доля перемещённых (рефакторинговых) строк — 25% (2021) → менее 10% (2024); копипаста — 8,3% → 12,3%; блоков дублирования из 5+ строк — примерно в 8 раз больше за 2024 год). Отсутствующая обработка ошибок прикручивается позже, дублируется в каждом месте вызова и никогда не централизуется.

##Лечение

Тактики ревью и промптинга

  • Заставьте модель сначала перечислить режимы сбоев. Промпт: "Прежде чем писать код, перечисли режимы сбоев для этой функции (плохой ввод, сетевая ошибка, не-200, пустой/некорректный ответ, отсутствующие поля, конкурентность). Затем реализуй обработку для каждого". Принуждение к шагу перечисления противодействует склонности следующего токена их пропускать.
  • Укажите на соглашения для переиспользования. "Используй наш существующий тип AppError/Result из lib/errors.ts и logger из lib/log.ts; валидируй ответ схемой zod из schemas/user.ts". Это превращает "отсутствие обработки" в переиспользование вместо самодельного кома (избегая дублирования кода).
  • Требуйте прохождения проверок. "Запусти eslint и tsc --noEmit и исправь каждое предупреждение, включая @typescript-eslint/no-floating-promises". Проверка типов с strictNullChecks превращает тихие разыменования null в ошибки компиляции, которые модель обязана устранить.
  • Требуйте негативных тестов. Просите модульные тесты, покрывающие пустой/null/ошибочный ввод, а не только счастливый случай — отсутствие этих тестов само по себе является запахом.

Рефакторинг

Добавьте валидацию на границах и охранные выражения и централизуйте обработку. Именованные приёмы: охранные выражения, как в приёме «Замена вложенных условных операторов граничным оператором» (ранний throw при недопустимом состоянии), «Введение проверки утверждения» вместе с валидацией на границе (parse-don't-validate на краях ввода-вывода) и «Введение Null-объекта» (особое значение вроде ?? "UNKNOWN" вместо падения).

// после: режимы сбоев — первоклассные граждане
async function getUserCity(userId: string): Promise<string> {
  if (!userId) throw new InvalidArgumentError("userId required");   // охранное выражение

  const res = await fetch(`/api/users/${encodeURIComponent(userId)}`);
  if (!res.ok) throw new ApiError(`user fetch failed: ${res.status}`);

  const user = UserSchema.parse(await res.json());                  // валидируем на границе
  return user.address?.city?.toUpperCase() ?? "UNKNOWN";           // обрабатываем пробел как особый случай
}

Если один и тот же паттерн try/validate/log начинает повторяться в местах вызова, вынесите его с помощью приёма «Извлечение метода» в общую вспомогательную функцию fetchJson<T>(url, schema), чтобы обработка ошибок жила в одном месте, а не копировалась (ловушка дублирования GitClear).

##Обнаружение анализаторами

  • typescript-eslint @typescript-eslint/no-floating-promises — Отмечает промисы, путь отклонения которых никогда не обрабатывается (нет await/catch) — типичный симптом счастливого пути для асинхронных вызовов.
  • ESLint no-empty — Отмечает пустые блоки, включая пустые блоки catch — заглушку 'обработки ошибок', которую модель оставляет после себя.
  • SonarSource (JS/TS) javascript:S2486 — Исключения не должны игнорироваться — отмечает пойманные, но проглоченные ошибки, вырожденный родственник полного отсутствия обработки.