Как предотвращать ошибки: лучшие практики

Как предотвращать ошибки: лучшие практики

Технический фундамент профилактики ошибок

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

  • каждая функция явно описывает допустимый вход;
  • невалидные данные останавливаются как можно раньше (fail fast);
  • критичные ветки имеют fallback или явный отказ;
  • поведение проверяется на edge case до релиза.

Фундаментальная цель: сделать систему предсказуемой при любом входе, а не только на happy path.

Главная идея урока

Хорошая обработка ошибок важна, но еще лучше не доводить до сбоев там, где это можно предотвратить заранее. Предотвращение ошибок это набор привычек: валидировать вход, делать код предсказуемым, писать маленькие функции, покрывать критичные ветки тестами и не игнорировать предупреждения.

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

Проверь себя: почему баг, найденный до релиза, почти всегда дешевле багa из продакшена?

Валидация на входе

Большая часть runtime-ошибок появляется, когда функция получает неожиданные данные. Поэтому первая практика: проверяй вход как можно ближе к границе системы.

function createOrder(total, currency) {
  if (typeof total !== 'number' || Number.isNaN(total) || total < 0) {
    throw new Error('Некорректная сумма заказа');
  }

  if (typeof currency !== 'string' || currency.length !== 3) {
    throw new Error('Некорректная валюта');
  }

  return { total, currency: currency.toUpperCase() };
}

Смотри, что важно: валидируем не только тип, но и диапазон/формат. Проверка "это число" не спасает от NaN и отрицательных значений там, где они недопустимы.

Еще один нюанс: typeof total === 'number' не защищает от Infinity. Если тебе нужны только «обычные числа», удобнее проверять так:

function createOrder2(total, currency) {
  if (!Number.isFinite(total) || total < 0) {
    throw new Error('Некорректная сумма заказа');
  }
  // ...
}

Fail fast: падай рано и понятно

Принцип fail fast: если вход невалиден, останавливайся сразу с понятной причиной, а не протаскивай плохие данные дальше по системе.

Почему это полезно:

  • ошибка возникает ближе к источнику;
  • проще найти причину;
  • меньше вторичных поломок.

Проверь себя: что легче отладить - сбой в первой функции на проверке входа или падение через 10 шагов обработки?

Маленькие функции и явные контракты

Чем больше функция делает, тем сложнее понять, где именно она ломается. Разбивай логику на маленькие части с ясными ожиданиями.

function normalizeEmail(email) {
  if (typeof email !== 'string') return null;
  const value = email.trim().toLowerCase();
  return value.includes('@') ? value : null;
}

function buildUserPayload(rawEmail) {
  const email = normalizeEmail(rawEmail);
  if (!email) return { status: 'invalid_email' };
  return { status: 'ok', email };
}

Дополнительный пример: guard-клауза для обязательного объекта конфигурации.

function requireConfig(config) {
  if (!config || typeof config !== 'object') {
    throw new Error('Config object is required');
  }
  return config;
}

Смотри, что важно: typeof x === 'object' вернет true и для массивов. Если по контракту нужен именно объект-конфиг, часто добавляют проверку Array.isArray(config).

Одна функция отвечает за нормализацию, другая за сбор результата. Такой код проще тестировать и поддерживать.

Защитные значения и fallback

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

Мини-сценарий: настройки интерфейса.

  • Если парсинг сохраненных настроек не удался, возвращаем дефолтную тему.
  • Пользователь продолжает работу, а ошибка логируется для анализа.

Здесь важно не путать fallback и "замалчивание". Fallback это осознанная стратегия с понятным поведением.

Тесты на граничные случаи

Ошибки часто сидят на краях:

  • пустая строка;
  • null/undefined;
  • нулевые значения;
  • слишком большие/маленькие числа;
  • неожиданный формат данных.

Проверь себя: какие 3 edge case ты добавишь для функции, которая принимает возраст пользователя?

Даже несколько простых тестов на края уже сильно снижают риск регрессий после рефакторинга.

Чеклист предотвращения ошибок перед релизом

  1. Входные данные валидируются на границе модуля.
  2. Ошибки разделены на ожидаемые и неожиданные.
  3. Для критичных мест есть fallback или понятный сценарий отказа.
  4. Логи содержат контекст без чувствительных данных.
  5. Добавлены тесты на ключевые edge case.

Этот чеклист особенно полезен для форм, платежей, авторизации и любых сценариев с внешним API.

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

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

Анти-провал: лучше добавить 5 простых проверок на входе, чем потом ловить неочевидный баг в середине пайплайна.

Что будет, если изменить входные данные

В createOrder:

  • total = 1999 и currency = 'usd' -> успех;
  • total = -10 -> ошибка про сумму;
  • currency = 'rub1' -> ошибка про формат валюты.

Это предсказуемый контракт. Когда поведение заранее определено для плохих входов, система становится устойчивее.

Краткий итог

  • Предотвращение ошибок начинается с валидации входных данных.
  • Принцип fail fast помогает ловить проблемы ближе к источнику.
  • Маленькие функции и явные контракты делают код стабильнее.
  • Fallback полезен, если он осознанный и наблюдаемый через логи.
  • Тесты на edge case и простой чеклист перед релизом значительно снижают риск багов.