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

Как выбросить исключение наверх

2.0 Middle🔥 141 комментариев
#Основы Java

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

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

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

Как правильно выбросить исключение наверх в Java

Это важный навык, но часто делается неправильно. Существует несколько способов, и каждый имеет смысл в определённом контексте.

1. Простой throws (checked исключения)

Для checked исключений:

public class FileReader {
    
    // Способ 1: просто пробросить через throws
    public String readFile(String path) throws IOException {
        BufferedReader reader = new BufferedReader(new java.io.FileReader(path));
        return reader.readLine();
        // Если IOException — будет выброшено в вызывающий код
    }
}

// Использование
public class Application {
    public static void main(String[] args) throws IOException {
        FileReader reader = new FileReader();
        String content = reader.readFile("data.txt"); // IOException может быть выброшен
    }
}

Проблема: throws IOException распространяет ответственность выше по цепочке. Со временем вся цепочка объявляет throws IOException.

2. Обёртывание в RuntimeException (современный подход)

Для unchecked исключений. РЕКОМЕНДУЕТСЯ для большинства приложений:

public class UserRepository {
    
    // ✅ ХОРОШО — используем unchecked исключение
    public User findById(UUID id) {
        try {
            String query = "SELECT * FROM users WHERE id = ?";
            return jdbcTemplate.queryForObject(query, new UserRowMapper(), id);
        } catch (DataAccessException e) {
            throw new UserNotFoundException("User not found: " + id, e);
        } catch (Exception e) {
            throw new RuntimeException("Database error", e);
        }
    }
}

// Вызывающий код НЕ обязан ловить исключение
@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable UUID id) {
        // Исключение поднимется в @ControllerAdvice
        // Не нужно обрабатывать здесь
        User user = userRepository.findById(id);
        return ResponseEntity.ok(user);
    }
}

3. Перехват и переброс с дополнительной информацией

Это очень важно для сохранения контекста:

public class OrderService {
    
    public Order createOrder(OrderRequest request) {
        try {
            // Этап 1: валидация
            validateOrder(request);
            
            // Этап 2: проверка инвентаря
            checkInventory(request.getProductIds());
            
            // Этап 3: создание платежа
            Payment payment = paymentService.charge(request.getAmount());
            
            // Этап 4: сохранение
            return orderRepository.save(new Order(request, payment));
            
        } catch (PaymentException e) {
            // Перехватываем конкретное исключение
            // Добавляем контекст
            throw new OrderCreationException(
                "Payment failed for user " + request.getUserId() + 
                " with amount " + request.getAmount(),
                e
            );
        } catch (InventoryException e) {
            // Другой тип ошибки
            throw new OutOfStockException(
                "Product " + e.getProductId() + " is out of stock",
                e
            );
        }
    }
}

4. Re-throw с сохранением stack trace

// ❌ ПЛОХО — теряется оригинальный stack trace
public void processData() {
    try {
        loadData();
    } catch (IOException e) {
        throw new RuntimeException("Failed to load"); // Stack trace потерян!
    }
}

// ✅ ПРАВИЛЬНО — сохраняем оригинальное исключение
public void processData() {
    try {
        loadData();
    } catch (IOException e) {
        throw new RuntimeException("Failed to load", e); // e это cause
    }
}

// Другой способ
public void processData() throws IOException {
    try {
        loadData();
    } catch (IOException e) {
        // Re-throw оригинальное исключение
        throw e;
    }
}

5. Multi-catch для выброса разных исключений

public class DataProcessor {
    
    public void processFile(String filename) {
        try {
            validateFile(filename);
            readFile(filename);
            parseData();
        } catch (FileNotFoundException | SecurityException e) {
            // Разные исключения, но обрабатываем одинаково
            throw new DataProcessingException(
                "Cannot access file: " + filename,
                e
            );
        } catch (IOException e) {
            throw new DataProcessingException(
                "IO error while reading: " + filename,
                e
            );
        } catch (ParseException e) {
            throw new DataProcessingException(
                "Invalid data format in: " + filename,
                e
            );
        }
    }
}

6. Условный выброс

Иногда нужно выбросить исключение только в определённых условиях:

public class AccountService {
    
    public void withdraw(Account account, BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        
        if (account.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException(
                "Insufficient funds: have " + account.getBalance() + 
                ", need " + amount
            );
        }
        
        account.setBalance(account.getBalance().subtract(amount));
    }
}

7. Выброс в компонентах Spring

В Spring контексте исключения часто обрабатываются @ControllerAdvice:

@Service
public class UserService {
    
    @Transactional
    public User createUser(UserRequest request) {
        // Любое исключение в @Transactional будет откачено
        
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new EmailAlreadyExistsException("Email: " + request.getEmail());
        }
        
        if (request.getAge() < 18) {
            throw new IllegalArgumentException("User must be 18+");
        }
        
        return userRepository.save(new User(request));
    }
}

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    @PostMapping
    public ResponseEntity<UserResponse> createUser(@RequestBody UserRequest request) {
        // Не ловим исключение — пусть поднимется
        User user = userService.createUser(request);
        return ResponseEntity.status(201).body(new UserResponse(user));
    }
}

// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(EmailAlreadyExistsException.class)
    public ResponseEntity<ErrorResponse> handleEmailExists(EmailAlreadyExistsException e) {
        return ResponseEntity.status(409).body(
            new ErrorResponse("Conflict", e.getMessage())
        );
    }
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body(
            new ErrorResponse("Bad Request", e.getMessage())
        );
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception e) {
        log.error("Unexpected error", e);
        return ResponseEntity.status(500).body(
            new ErrorResponse("Internal Server Error", "An error occurred")
        );
    }
}

8. Пример: пробросить наверх с логированием

public class EmailService {
    
    private static final Logger log = LoggerFactory.getLogger(EmailService.class);
    
    public void sendEmail(String to, String subject, String body) {
        try {
            SmtpClient client = new SmtpClient();
            client.send(to, subject, body);
        } catch (SmtpException e) {
            // Логируем ошибку
            log.error("Failed to send email to {} with subject {}", to, subject, e);
            
            // Пробрасываем наверх (но уже залогирована)
            throw new EmailSendException(
                "Failed to send email to: " + to,
                e
            );
        } catch (Exception e) {
            log.error("Unexpected error while sending email", e);
            throw new RuntimeException("Email service error", e);
        }
    }
}

9. НЕ ДЕЛАЙ ТАК (anti-patterns)

// ❌ Плохо: ловишь и ничего не делаешь
try {
    database.connect();
} catch (Exception e) {
    // Молчишь — ошибка потеряется
}

// ❌ Плохо: печатаешь в консоль
try {
    database.connect();
} catch (Exception e) {
    System.out.println("Error: " + e.getMessage()); // Нет логирования
}

// ❌ Плохо: выбрасываешь новое исключение без причины
try {
    database.connect();
} catch (Exception e) {
    throw new RuntimeException("Error"); // Потеял оригинальное исключение
}

// ❌ Плохо: ловишь Exception
try {
    // код
} catch (Exception e) { // Слишком широко
    // ловишь и OutOfMemoryError, и StackOverflowError
}

10. Best Practice итоговый пример

public class PaymentProcessor {
    
    private static final Logger log = LoggerFactory.getLogger(PaymentProcessor.class);
    
    public void processPayment(Payment payment) {
        try {
            // Бизнес-логика
            validatePayment(payment);
            chargeCard(payment);
            updateDatabase(payment);
            
        } catch (InvalidPaymentException e) {
            // Ошибка валидации — это OK
            throw e; // или оборачиваем в доменное исключение
            
        } catch (CardDeclinedException e) {
            // Карта отклонена — ожидаемая ситуация
            log.warn("Card declined for user {}", payment.getUserId());
            throw new PaymentFailedException("Card declined", e);
            
        } catch (DatabaseException e) {
            // Ошибка БД — логируем и выбрасываем
            log.error("Database error while processing payment {}", payment.getId(), e);
            throw new PaymentProcessingException(
                "Failed to update payment status",
                e
            );
            
        } catch (Exception e) {
            // Неожиданная ошибка
            log.error("Unexpected error in payment processing", e);
            throw new PaymentProcessingException(
                "Unexpected error",
                e
            );
        }
    }
}

Выводы

  1. Используй unchecked исключения (extends RuntimeException) в современном коде
  2. Всегда сохраняй оригинальное исключение как cause: throw new MyException(msg, e)
  3. Добавляй контекст при переброске: информацию о том, что делалось
  4. Логируй на правильном уровне: warn для ожидаемых, error для неожиданных
  5. Не ловишь Exception — ловишь конкретные исключения
  6. Используй @ControllerAdvice для глобальной обработки в Spring
  7. Не подавляй исключения молча — либо обработай, либо пробрось
Как выбросить исключение наверх | PrepBro