惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
量子位
D
DataBreaches.Net
博客园 - 司徒正美
J
Java Code Geeks
博客园 - 【当耐特】
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
aimingoo的专栏
aimingoo的专栏
B
Blog
The Cloudflare Blog
D
Docker
I
InfoQ
爱范儿
爱范儿
MongoDB | Blog
MongoDB | Blog
腾讯CDC
月光博客
月光博客
Hugging Face - Blog
Hugging Face - Blog
Microsoft Azure Blog
Microsoft Azure Blog
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
小众软件
小众软件
S
SegmentFault 最新的问题
GbyAI
GbyAI
有赞技术团队
有赞技术团队

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Правильное использование механизма исключений
Александр · 2026-06-15 · via Все публикации подряд на Хабре

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

В первую очередь следует понимать, что исключения — это синтаксический сахар. Реализовать обработку ошибок можно и обычными if...else, но кода будет намного больше. А если механизм исключений используется неправильно, то преимущества такого синтаксического сахара просто не используются, вы получаете тот же if...else, только обогащаете код «ненужным в этом случае синтаксисом».

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

Например, в следующем коде определяется собственное исключение, сообщающее, что ресурс не найден. По типу исключения мы понимаем его суть, а по полям user и resourceId мы понимаем, кто пытался запросить ресурс и какой именно. Имея такую информацию в логах, становится сильно проще разбираться в причинах проблемы.

public class ResourceNotFoundException extends RuntimeException {
  private final User user;
  private final String resourceId;

  public ResourceNotFoundException(String resourceId, User user) {
    super("Ресурс с id " + resourceId + " не найден. Пользователь: " + user.toString());
    this.resourceId = resourceId;
  }
}

В чем заключается синтаксический сахар исключений? А в том, что в цепочке вызовов методов не требуется прокидывать объект с информацией об ошибке. Между throw и try‑catch может быть сколько угодно промежуточных методов, которые ничего не будут знать об этом исключении.

Цепочка методов, через которые перебрасывается исключение

Цепочка методов, через которые перебрасывается исключение

Метод, организующий try‑catch, несет ответственность за обработку заданных исключений. Очень важно навешивать ответственность за обработку исключений архитектурно правильно для прозрачности кода. Более того, если метод несет ответственность за обработку исключений, он больше не должен на себя брать никакой ответственности, то есть логики в нем не должно быть.

Взглянем на следующий код:

public T execute(Object[] params) {
  try {
    Task task = generateTask();
    String id = task.id;
    if (params.size() > 0) {
      fixParams(id, params);
      return task.run(params);
    } else {
      return task.run();
    }
  } catch (Exception e) {
    log(e.getMessage());
  }
}

В этом коде прослеживается логика внутри блока try. Это затрудняет чтение особенно из‑за возникшего внутреннего условного оператора — дополнительный уровень вложенности.

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

public T execute(Object[] params) {
  try {
    executeTask(params);
  } catch (Exception e) {
    log(e.getMessage());
  }
}

Метод, организующий try catch блок, является замыкающим: вся цепочка методов, вызываемых после него, не имеет try catch блока совсем — там реализуется исключительно бизнес‑логика. Согласитесь, намного приятнее выглядит метод прошлого примера без try‑catch блока?

public T executeTask(Object[] params) {
  Task task = generateTask();
  String id = task.id;
  if (params.size() > 0) {
    fixParams(id, params);
    return task.run(params);
  } else {
    return task.run();
  }
}

Метод должен иметь минимальное количество уровней вложенности: лишний вложенный if уже затрудняет чтение, так же как и блок try. Идеальная структура метода, несущего ответственность за обработку исключений, выглядит следующим образом:

// Реализация метода
try {
  // Вызов метода-обработчика (одна строка)
} catch (MyException e) {
  // Логика обработки исключения (любое число строк)
} catch (MyException2 e) {
  // Логика обработки исключения 2 (любое число строк)
...
} catch (MyExceptionN e) {
  // Логика обработки исключения N (любое число строк)
}

В таком методе акцент только на обработке исключений. В блоке try ничего нет кроме вызова какой‑то операции. При этом обработка отдельного исключения реализуется одним из следующих вариантов:

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

  • Переброс исключения. Метод до‑обогащает исключение дополнительной информации, которая отсутствует в пойманном исключении. К примеру, для какого ID процесса произошла проблема. Переброс недопустим, если до‑обогащать новой информацией исключение не требуется. Единственное, в редких случаях допускается пере‑выбрасывать Exception в RuntimeException, чтобы устранить лишние throws.

  • Определение другого сценария поведения. Имеется ввиду, что в блоке catch реализуется полноценная логика. Например, не удалось преобразовать строку в json, поэтому в блоке catch идем по другому сценарию, где обрабатывается чистая строка. Такой вариант обработки исключений по возможности рекомендуется исключать, а пользоваться признаками или флагами для понимания, в какой сценарий необходимо уйти. Использование такого подхода сильно усложняет код, поскольку ожидаем обработку ошибки, а в итоге уходим в другой сценарий, как будто это ветка else.

  • Формирование результата. Если метод должен в любом случае вернуть какой‑то результат независимо от исключения, тогда в блоке catch может быть реализована логика формирования специфического ответа метода в случае ошибки. Такое часто применяется для REST, когда клиент в любом случае должен получить ответ. Тот же Spring благодаря магии рефлексии обеспечивает лаконичность благодаря handler‑ам исключений: мы вообще не пишем try‑catch, мы объявляем handler — отдельный метод, который вызывается в случае заданного исключения.

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

Логирование

try {
  // ...
} catch (MyException e) {
  // Логирование в случае получения кастомного исключения (пишем все доступные поля в лог)
  log(buildMessage(e.getProcessId(), e.getMessage()));
} catch (Exception e) {
  // Логирование всех остальных исключений с менее подробной информацией 
  // (исключения, которые разработчик не предусмотрел)
  log(e.getMessage());
}

Переброс исключения

try {
  // ...
} catch (ArgumentException e) {
  // Переброс исключения, до-обогащая processId
  throw new MyException(processId, e.getMessage());
}

Определение другого сценария поведения

try {
  JsonNode json = parseToJson(content);
  processJson(json);
} catch (ArgumentException e) {
  // Обработка строки
  processString(content);
}

Формирование результата

try {
  // Возвращаем успешный результат
  return ResponseEntity.body(execute(id));
} catch (Exception e) {
  // Возвращаем тело ошибки
  return ResponseEntity.body(buildError(e));
}

Для максимальной прозрачности кода рекомендуется документировать методы вместе с возможными исключениями, которые могут быть важны для пользователя. В Java мы можем использовать @throws для документации:

/**
 * Преобразовать перечень элементов в список строк
 * @param items Перечень элементов
 * @return Скомпонованный список
 * @throws NullPointerException Передан нулевой элемент или список
 * @throws IncorrectJsonException Не удается преобразовать в json элемент
 */
public ArrayList<String> flat(Iterable items, ObjectMapper mapper)

Теперь, если мы захотим вызвать этот метод, мы сразу увидим, к чему быть готовым. При этом указание исключения вовсе не означает, что надо его обрабатывать, мы можем выполнить предварительные проверки. Например, нас предупредили, что может быть NullPointerException, значит, нам надо убедиться, что наш код никак не передаст в качестве items null. То же касается и IncorrectJsonException — вероятно, мы проверки сделаем предварительно, и не придется выполнять try‑catch.

if (items != null)
  // Гарантируем, что items не равен null (try catch не нужен для NullPointerException)
  return flat(items, objectMapper);
else
  return new ArrayList<>();

Ключевое слово throws для объявляемых исключений, к сожалению, часто создает загромождение в коде. Иногда мы гарантируем, что исключение не может быть выброшено бизнес‑логикой, но ключевое слово throws все равно вынуждает нас писать try catch. В этом плане даже Роберт Мартин в своей книге «Чистый код» говорит о ненужности throws и рекомендует не работать с объявляемыми исключениями — пере‑выбрасывать в не объявляемые. Код ниже демонстрирует сигнатуру метода, который вынуждает обработать JsonProcessingException.

// Вынуждаем снаружи обрабатывать исключение JsonProcessingException, хотя наш код
// мог бы гарантировать, что такого исключения не может быть
public ArrayList<String> flat(Iterable items, ObjectMapper mapper) 
  throws JsonProcessingException

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

Хороших всем разработок!