W3docs

Лучшие практики обработки исключений в Java

Практические правила обработки исключений в Java — раннее завершение, правильные типы, не игнорируйте исключения и логируйте с пользой.

Предыдущие главы охватили механику — try, catch, finally, throw, throws, пользовательские классы. Эта глава посвящена суждению. Две программы могут использовать одни и те же конструкции, и одна будет надёжной, а другая — хрупкой. Разница заключается в небольшом наборе привычек, которые с практикой становятся рефлексом.

Быстро завершайте при некорректных входных данных

Если метод вызван с аргументами, которые он не может обработать, бросьте исключение немедленно:

public void send(String to, String body) {
  if (to == null || to.isBlank()) {
    throw new IllegalArgumentException("to must be non-blank");
  }
  if (body == null) {
    throw new NullPointerException("body");
  }
  // ...real work
}

Не пытайтесь «сделать всё возможное» с null. Ошибка на стороне вызывающего кода, и чем ближе исключение к нему, тем проще его исправить. Objects.requireNonNull(body, "body") — стандартная однострочная замена для случая с null.

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

Бросайте наиболее конкретный подходящий тип

Exception редко является правильным вариантом для броска, а RuntimeException — только когда нет более конкретного типа. Стандартная библиотека даёт вам словарь — используйте его:

  • Неверный аргумент → IllegalArgumentException
  • Null-аргумент → NullPointerException (Objects.requireNonNull)
  • Неверное состояние → IllegalStateException
  • Операция не поддерживается → UnsupportedOperationException
  • Индекс вне диапазона → IndexOutOfBoundsException
  • Число вне диапазона → ArithmeticException или IllegalArgumentException

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

Перехватывайте наиболее конкретный тип, для которого у вас есть план

Симметричное правило. catch (Exception e) — это запах кода. Оно перехватывает программные ошибки (NullPointerException, IllegalStateException), восстанавливаемые сбои (IOException) и неизвестные исключения библиотек в одной корзине — и почти всегда обрабатывает их одинаково, что почти всегда неверно.

// Bad — what does this even handle?
try { complex(); }
catch (Exception e) { log("failed"); }

// Better — specific cases get specific responses
try { complex(); }
catch (IOException e)         { retryLater(); }
catch (ParseException e)      { recordCorruptInput(e); }

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

Никогда не игнорируйте исключение молча

Худший паттерн обработки исключений:

try { doWork(); }
catch (Exception e) { }    // never write this

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

try { connection.close(); }
catch (IOException ignored) {
  // close-time failure on a cleanup path; original cause already propagating
}

Имя переменной ignored и комментарий делают намерение видимым для следующего читателя.

Логируйте с пользой, логируйте один раз

Распространены два недостатка логирования:

  • Логирование без достаточного контекстаlog.error("failed") ничего не говорит.
  • Логирование с последующим перебросом — каждый слой логирует одно и то же исключение, и один и тот же стек вызовов попадает в лог пять раз.

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

try {
  userService.activate(id);
} catch (UserNotFoundException e) {
  log.warn("activation failed: no user with id={}", id, e);   // include the exception object as the last arg
  return Response.notFound();
}

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

Сохраняйте причину при оборачивании

Когда вы транслируете исключение на уровень выше, всегда передавайте оригинал как причину:

// Good — cause is preserved
catch (IOException e) {
  throw new ConfigLoadException("failed to load " + path, e);
}

// Bad — original IOException is lost
catch (IOException e) {
  throw new ConfigLoadException("failed to load " + path);
}

Цепочка Caused by: в результирующем стеке вызовов позволяет дежурному инженеру отследить доменное исключение до сбоя на уровне байтов. Потеряйте её — и получасовая отладка превратится в полдня.

Не используйте исключения для управления потоком

Бросок исключений дорог — построение стека вызовов при создании является основной стоимостью. Что важнее, это затуманивает намерение. Цикл, использующий try/catch (NoSuchElementException), чтобы знать, когда остановиться, скрывает то, что делает:

// Bad
try {
  while (true) {
    process(iter.next());
  }
} catch (NoSuchElementException end) { }

// Good
while (iter.hasNext()) {
  process(iter.next());
}

Когда «не найдено» — обычный результат, возвращайте Optional<T> или boolean. Оставьте исключения для действительно исключительного.

Используйте finally и try-with-resources для очистки

finally должен освобождать ресурсы. try-with-resources должен быть выбором по умолчанию для всего, что реализует AutoCloseable. Не помещайте бизнес-логику в finally — он выполняется и на пути успеха, и на пути ошибки и не может различить их. И не делайте return из finally — это молча отбрасывает оригинальное исключение или возвращаемое значение, что является одной из сложнейших для диагностики ошибок.

Документируйте то, что бросаете

Если метод может бросить исключение, важное для вызывающих — проверяемое или нет — скажите об этом в Javadoc:

/**
 * Looks up a user by id.
 *
 * @throws UserNotFoundException if no user with that id exists
 * @throws IllegalArgumentException if id is null or blank
 */
public User lookup(String id) { ... }

Компилятор применяет это для проверяемых исключений в сигнатуре. Для непроверяемых Javadoc — единственный контракт, и вызывающие действительно нуждаются в нём, когда исключение влияет на то, как они должны использовать метод.

Используйте исключения для сбоев, возвращаемые значения — для обычных результатов

Обобщающее правило, связывающее всё остальное. Исключение говорит что-то пошло не так, и я не могу это исправить здесь. Возвращаемое значение говорит вот результат. Когда «не найдено» является частью нормальной работы, возвращайте Optional.empty(), boolean или сигнальное значение. Когда «соединение с базой данных разорвано» — бросайте.

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

Рабочий пример

Небольшая функция обработки заказов, объединяющая практики этой главы — быстрая проверка входных данных, конкретные встроенные типы исключений, оборачивание с причиной и единственный верхнеуровневый обработчик, логирующий один раз.

java— editable, runs on the server

Четыре вызова, четыре разных пути. Успешный завершается нормально. Два случая IllegalArgumentException (null-идентификатор, нулевая сумма) сообщаются с сообщением, объясняющим что было неверным во входных данных. Симулированный сбой сервиса всплывает как доменное OrderProcessingException с оригинальным IllegalStateException, связанным через getCause(). Ничего не проглочено, ничего не залогировано дважды, и каждый сбой точно указывает, какое значение его вызвало.

Что дальше

На этом завершается часть 8 — у вас есть рабочее понимание механики исключений Java и суждение для их правильного использования. Следующая часть посвящена глубокому изучению строк — наиболее распространённого типа в коде Java с гораздо большей глубиной, чем кажется на поверхности. Перейдите к классу Java String.

Практика

Практика
Какая из этих привычек обработки исключений **наиболее** вероятно сделает отладку в продакшене болезненной?
Какая из этих привычек обработки исключений **наиболее** вероятно сделает отладку в продакшене болезненной?
Was this page helpful?