← Назад к вопросам

Почему не следует обрабатывать Unchecked?

2.0 Middle🔥 181 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью#ORM и Hibernate

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему не следует перехватывать Unchecked Exceptions

Это важный принцип обработки ошибок в Java. Объясню, почему Unchecked исключения нельзя ловить обычным образом.

Что такое Unchecked Exceptions

Unchecked Exceptions (наследники RuntimeException):

// Unchecked (не нужно объявлять в throws)
NullPointerException
ArrayIndexOutOfBoundsException
ClassCastException
IllegalArgumentException
ArithmeticException
NumberFormatException
UnsupportedOperationException

// Checked (НУЖНО объявлять в throws или ловить)
IOException
SQLException
FileNotFoundException

Основной принцип

Unchecked исключения указывают на ОШИБКИ В КОДЕ, а не внешние проблемы.

Поэтому их не нужно перехватывать - нужно исправить код.

Проблема 1: Скрытие бага

Перехват Unchecked скрывает проблему в коде:

// ❌ ПЛОХО - скрываем NullPointerException
public String getUserName(User user) {
    try {
        return user.getName();  // Может быть NPE
    } catch (NullPointerException e) {
        return "Unknown";  // Скрыли ошибку!
    }
}

// Использование
User user = null;
String name = getUserName(user);  // Вернёт "Unknown"
// Но реально это ОШИБКА в коде!
// null не должен попадать сюда

// ✅ ХОРОШО - предотвратить проблему
public String getUserName(User user) {
    if (user == null) {
        throw new IllegalArgumentException("User не может быть null");
    }
    return user.getName();
}

// Использование
User user = null;
String name = getUserName(user);  // IllegalArgumentException сразу!
// Баг обнаружен в месте ошибки

Проблема 2: Непредсказуемый код

Перехватываем Exception неизвестного происхождения:

// ❌ Ловим RuntimeException - слишком широко
try {
    String[] names = {"Alice", "Bob"};
    System.out.println(names[5]);  // Может быть ArrayIndexOutOfBoundsException
    int result = Integer.parseInt("not a number");  // Может быть NumberFormatException
    Object obj = "string";
    Integer num = (Integer) obj;  // Может быть ClassCastException
} catch (RuntimeException e) {
    System.out.println("Something went wrong");
    // ❌ Какая именно ошибка? Не знаем!
    // Может быть всё что угодно
}

// ✅ ХОРОШО - проверять ДО кода
public void safeParse(String input) {
    if (input == null || input.isEmpty()) {
        throw new IllegalArgumentException("Input не может быть пустой");
    }
    
    try {
        int num = Integer.parseInt(input);  // Checked ошибка
    } catch (NumberFormatException e) {
        // Знаем точно - это ошибка парсинга
        throw new IllegalArgumentException("Invalid number format", e);
    }
}

Проблема 3: Нарушение fail-fast

Принцип fail-fast: ошибка должна упасть быстро, в месте возникновения.

// ❌ Перехватываем - замораживаем ошибку
public void processData(String[] items) {
    try {
        for (int i = 0; i < 100; i++) {
            String item = items[i];  // ArrayIndexOutOfBoundsException на i=5
            processItem(item);
        }
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("Process completed");
    }
}

// Результат:
// - Цикл остановился на i=5
// - Остальные 95 элементов не обработаны
// - Мы думаем "всё ОК" потому что catch обработал
// - БУГ! Молчание

// ✅ ХОРОШО - проверять ДО кода
public void processData(String[] items) {
    if (items == null || items.length == 0) {
        throw new IllegalArgumentException("Items не может быть пустой");
    }
    
    for (int i = 0; i < items.length; i++) {
        String item = items[i];  // Теперь безопасно
        processItem(item);
    }
}

Проблема 4: NullPointerException преследует

Самое частое Unchecked исключение - NPE:

// ❌ ПЛОХО - перехватываем NPE
public String getCity(User user) {
    try {
        return user.getAddress().getCity();  // NPE на user.getAddress()
    } catch (NullPointerException e) {
        return "Unknown";  // Скрыли ошибку
    }
}

// ✅ ХОРОШО - проверяем шаг за шагом
public String getCity(User user) {
    if (user == null) {
        throw new IllegalArgumentException("User не может быть null");
    }
    
    Address address = user.getAddress();
    if (address == null) {
        return "Unknown address";  // Ожидаемый сценарий
    }
    
    return address.getCity();
}

// ✅ ЕЩЁ ЛУЧШЕ - Java 8+ Optional
public String getCity(User user) {
    return Optional.ofNullable(user)
        .map(User::getAddress)
        .map(Address::getCity)
        .orElse("Unknown");
}

Проблема 5: Потеря stack trace

Перехват исключения часто теряет важную информацию:

// ❌ ПЛОХО - теряем информацию
try {
    int[] nums = {1, 2, 3};
    int value = nums[10];  // ArrayIndexOutOfBoundsException
} catch (RuntimeException e) {
    e.printStackTrace();  // Выведет "Index 10 out of bounds for length 3"
    // Но что делать дальше? Приложение продолжит работать неправильно
}

System.out.println("All OK");  // ❌ Это выведется, но это ошибка!

// ✅ ХОРОШО - не перехватываем, падаем
int[] nums = {1, 2, 3};
if (nums.length > 10) {
    throw new IllegalArgumentException("Индекс вне границ");
}
int value = nums[10];  // Безопасно

Практические примеры неправильных ловушек

1. Ловушка "поймать всё":

// ❌ ОЧЕНЬ ПЛОХО
try {
    // Весь код приложения
} catch (Exception e) {  // Включает RuntimeException!
    e.printStackTrace();
}

// Это скроет ВСЕ ошибки - даже критические

// ✅ Только если нужно:
try {
    // IO операции
} catch (IOException e) {  // Конкретный тип
    throw new UncheckedIOException(e);
}

2. Ловушка classcast:

// ❌ ПЛОХО
try {
    List<String> list = (List<String>) someObject;
} catch (ClassCastException e) {
    list = new ArrayList<>();  // Скрыли ошибку типизации
}

// ✅ ХОРОШО - проверка ДО кода
List<String> list;
if (someObject instanceof List) {
    list = (List<String>) someObject;
} else {
    throw new IllegalArgumentException("Expected List, got " + someObject.getClass());
}

Когда можно перехватить RuntimeException

1. На граница приложения (servlet, controller):

// ✅ ХОРОШО - последняя линия защиты
@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        try {
            User user = userService.getUser(id);
            return ResponseEntity.ok(mapper.toDTO(user));
        } catch (RuntimeException e) {
            // Логируем ошибку для дебага
            logger.error("Unexpected error", e);
            // Возвращаем HTTP 500
            return ResponseEntity.status(500).build();
        }
    }
}

2. Логирование для диагностики:

// ✅ ХОРОШО - логируем, но не скрываем
public void processWithLogging(Task task) {
    try {
        processTask(task);
    } catch (RuntimeException e) {
        // Логируем для анализа
        logger.error("Failed to process task: " + task.getId(), e);
        // Пробрасываем дальше
        throw e;
    }
}

3. Очистка ресурсов (try-finally):

// ✅ ХОРОШО - не подавляем исключение
List<String> list = new ArrayList<>();
try {
    // Код
} finally {
    list.clear();  // Очищаем ресурсы
    // Исключение пройдёт дальше
}

// ✅ Ещё лучше - try-with-resources
try (FileReader file = new FileReader("file.txt")) {
    // Использование
}  // Файл закроется автоматически

Правильная обработка ошибок

// Иерархия обработки

public class OrderService {
    
    // 1. Validation - проверить входные данные
    public void createOrder(CreateOrderRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Request не может быть null");
        }
        if (request.getAmount() <= 0) {
            throw new IllegalArgumentException("Amount должен быть > 0");
        }
        
        try {
            // 2. Business logic - обработка
            Order order = new Order(request);
            
            try {
                // 3. External calls - внешние сервисы
                paymentService.processPayment(order);
            } catch (PaymentException e) {  // Checked exception!
                throw new OrderProcessingException("Payment failed", e);
            }
            
        } catch (RuntimeException e) {
            // 4. Logging - логируем на границе
            logger.error("Order creation failed", e);
            throw e;  // Не скрываем!
        }
    }
}

Правило большого пальца

Если вы пишете catch RuntimeException - подумайте:

Вопрос: Почему я ловлю это исключение?

Ответ 1: "Чтобы приложение не упало"
❌ НЕПРАВИЛЬНО - значит есть баг в коде

Ответ 2: "Чтобы залогировать"
❌ Логируй, но пробрасывай дальше (catch + throw)

Ответ 3: "Чтобы вернуть graceful error response"
✅ ПРАВИЛЬНО - это должно быть на границе (controller)

Ответ 4: "Чтобы очистить ресурсы"
✅ ПРАВИЛЬНО - используй finally или try-with-resources

Заключение

Почему не следует перехватывать Unchecked Exceptions:

  1. Скрытие багов - ошибка в коде должна падать
  2. Нарушение fail-fast - остановка в месте ошибки
  3. Потеря информации - не понимаем, что случилось
  4. Непредсказуемый код - скрытые зависимости
  5. Нарушение контракта - код обещает падать на ошибке

Правила:

  1. Не ловите RuntimeException (если не на границе)
  2. Проверяйте аргументы ДО кода - throw IllegalArgumentException
  3. Используйте Optional вместо try-catch для null
  4. Логируйте на границе (controller, servlet)
  5. Пробрасывайте исключение после логирования
  6. Очищайте ресурсы в finally или try-with-resources

Запомните:

Unchecked exceptions = ошибка в коде Checked exceptions = внешние проблемы

Ошибки в коде нужно исправлять, а не скрывать!

Почему не следует обрабатывать Unchecked? | PrepBro