Структура и обработка ошибок
Что это такое
Структура — это порядок, в котором читатель встречает вещи: что идёт первым, где живут детали, как устроен файл. К несчастливому пути применима та же структура, и тогда она называется обработкой ошибок: нужно решить, где сбой обнаружен, где он превращается в решение и где перестаёт быть проблемой вызывающего; чистый код во многом и есть привычка держать обычный путь видимым, а путь отказа — явным.
Почему это важно
Большинство производственных ошибок живёт не в алгоритме, а в разрыве между тем, как код читается, и тем, что машина делает на краях. Проглоченное исключение превращает ясный сбой в загадку в двух системах отсюда. Похожим образом функция, которая перехватывает ошибку, чтобы продолжать, и возвращает правдоподобное значение по умолчанию, — именно так неверное число и доходит до клиента. Порядок тоже важен: код, в котором перемешаны проверка, преобразование и сохранение, — источник наполовину применённого изменения. А верная структура делает путь отказа тем, о чём можно рассуждать, а не на что надеяться.
Ключевые идеи
Один уровень абстракции на функцию
Не смешивайте в одном блоке сетевой вызов, обрезку строки и запись в базу. Каждая строка функции-координатора должна быть вызовом примерно того же уровня, а детали пусть уходят в именованные помощники. Тогда читатель доверяет сводке и спускается лишь туда, куда ему действительно нужно.
async function publishDraft(draft) {
const validated = validateDraft(draft);
const record = toRecord(validated);
await repository.save(record);
return record.id;
}
Сбой там, где есть информация
Обрабатывайте ошибку на том уровне, который знает, что с ней делать. Низкоуровневая функция обязана точно сообщить, что пошло не так; средний слой — добавить контекст, который есть только у него; а слой, владеющий операцией, решает, повторять, заменять или отказываться. Если перехватить ошибку рано и вернуть смутное значение, вся информация, нужная слоям выше, пропадёт.
try {
await paymentGateway.charge(order);
} catch (cause) {
throw new PaymentFailed(`order ${order.id}`, { cause });
}
Пусть счастливый путь течёт вниз
Защитные условия лучше вложенных ветвлений. Проверяйте неверные случаи первыми и выходите сразу — тогда дальше пойдёт обычный случай на одном уровне отступа. Глубокая вложенность опасна тем, что читатель теряет, в каком сочетании условий он находится, и начинает вносить неверные правки.
function discountFor(customer) {
if (!customer) return 0;
if (!customer.active) return 0;
return customer.years * 0.01;
}
Не проглатывайте и не перехватывайте лишнее
Пустой блок catch — это обещание, что программа продолжит работать правильно, хотя она не знает, почему сбойнула. Перехват широкой категории ошибок и одинаковое обращение со всеми — та же проблема, только в большем костюме. Перехватывайте то, что понимаете, а всё остальное пусть идёт вверх вместе со своей исходной причиной.
try {
return JSON.parse(raw);
} catch {
return null; // приемлемо только потому, что null — настоящее, обработанное значение
}
Ошибки — это значения, которые можно проектировать
Выброшенное исключение, возвращённый тип-результат или явное поле ошибки — всё это выборы с последствиями. Выброшенная ошибка невидима в сигнатуре, и о ней легко забыть. Возвращаемый результат, наоборот, вынуждает вызывающего разобраться и делает сбой частью задокументированного вывода функции. Выбирайте осознанно, держитесь одного выбора в пределах модуля и не позволяйте двум соглашениям протекать в одну цепочку вызовов.
Практика
Переупорядочьте одну функцию ради читателя
Возьмите длинную функцию, которая проверяет, вычисляет и сохраняет. Переупорядочьте её так, чтобы неверные случаи защищались вначале, вычисление стояло посередине за ясным именем, а сохранение было последним оператором.
Готово, когда: обычный путь читается сверху вниз без вложенности глубже одного уровня, а поведение не изменилось.
Сделайте проглоченный сбой видимым
Найдите в своём коде блок catch, который возвращает значение по умолчанию. Добавьте исходную причину к тому, что выбрасывается или пишется в журнал, и дайте сбою имя, говорящее, какая операция не удалась.
Готово, когда: принудительный сбой этого вызова даёт сообщение, называющее операцию и нижележащую причину.
Частые ошибки
- Перехват ошибки только чтобы записать её и продолжить — программа идёт дальше в неизвестном состоянии, и ущерб всплывает лишь позже; исправить: решите явно, может ли вызывающий продолжать, а если нет — перебросьте ошибку вместе с контекстом.
- Возврат значения по умолчанию при сбое — правдоподобный ноль или пустой список скрывают сломанную операцию; исправить: возвращайте результат, который вызывающий обязан истолковать, или падайте громко.
- Смешение проверки, работы и сохранения в одном блоке — сбой на середине оставляет частичный эффект; исправить: разделите фазы, чтобы порядок стал виден.
- Вложенные условия вместо защитных — читателю приходится держать в голове все сочетания сразу; исправить: выходите сразу для неверных случаев.
- Исключения для обычного потока — отсутствующее необязательное значение превращается в дорогое и шумное событие; исправить: возвращайте значение для ожидаемых исходов, а исключения оставьте исключительному.
Что читать дальше
- Имена и маленькие функции (01-cs-fundamentals/08-clean-code/01-names-and-small-functions) — привычки насчёт размера и имён, которые делают такой порядок возможным, поэтому читайте это первым.
- Отладка (01-cs-fundamentals/11-debugging) — что даёт сбой, который точно сообщает о себе, когда что-то идёт не так.
- Код-ревью (01-cs-fundamentals/17-code-review) — рецензирование диффа именно на проглоченные сбои и неясный порядок.