Структура и обработка ошибок

Что это такое

Структура — это порядок, в котором читатель встречает вещи: что идёт первым, где живут детали, как устроен файл. К несчастливому пути применима та же структура, и тогда она называется обработкой ошибок: нужно решить, где сбой обнаружен, где он превращается в решение и где перестаёт быть проблемой вызывающего; чистый код во многом и есть привычка держать обычный путь видимым, а путь отказа — явным.

Почему это важно

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

Ключевые идеи

Один уровень абстракции на функцию

Не смешивайте в одном блоке сетевой вызов, обрезку строки и запись в базу. Каждая строка функции-координатора должна быть вызовом примерно того же уровня, а детали пусть уходят в именованные помощники. Тогда читатель доверяет сводке и спускается лишь туда, куда ему действительно нужно.

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) — рецензирование диффа именно на проглоченные сбои и неясный порядок.