Как предотвращать ошибки: лучшие практики
Как предотвращать ошибки: лучшие практики
Технический фундамент профилактики ошибок
Предотвращение ошибок строится вокруг инвариантов и границ модуля:
- каждая функция явно описывает допустимый вход;
- невалидные данные останавливаются как можно раньше (
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 ты добавишь для функции, которая принимает возраст пользователя?
Даже несколько простых тестов на края уже сильно снижают риск регрессий после рефакторинга.
Чеклист предотвращения ошибок перед релизом
- Входные данные валидируются на границе модуля.
- Ошибки разделены на ожидаемые и неожиданные.
- Для критичных мест есть fallback или понятный сценарий отказа.
- Логи содержат контекст без чувствительных данных.
- Добавлены тесты на ключевые edge case.
Этот чеклист особенно полезен для форм, платежей, авторизации и любых сценариев с внешним API.
Типичные ошибки новичков
- Доверять данным "потому что раньше приходили нормальные".
- Писать одну большую функцию на весь сценарий.
- Проверять только happy path, игнорируя края.
- Считать, что
try...catchзаменяет качественную архитектуру.
Анти-провал: лучше добавить 5 простых проверок на входе, чем потом ловить неочевидный баг в середине пайплайна.
Что будет, если изменить входные данные
В createOrder:
total = 1999иcurrency = 'usd'-> успех;total = -10-> ошибка про сумму;currency = 'rub1'-> ошибка про формат валюты.
Это предсказуемый контракт. Когда поведение заранее определено для плохих входов, система становится устойчивее.
Краткий итог
- Предотвращение ошибок начинается с валидации входных данных.
- Принцип
fail fastпомогает ловить проблемы ближе к источнику. - Маленькие функции и явные контракты делают код стабильнее.
- Fallback полезен, если он осознанный и наблюдаемый через логи.
- Тесты на edge case и простой чеклист перед релизом значительно снижают риск багов.