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

Как сделать механизм обработки ошибок

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

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

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

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

Механизм обработки ошибок в Java: полный разбор

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

Иерархия исключений в Java

Throwable
├── Error (не ловим!)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── VirtualMachineError
└── Exception (ловим мы)
    ├── Checked exceptions (необходимо обработать)
    │   ├── IOException
    │   ├── SQLException
    │   └── custom checked exceptions
    └── Unchecked exceptions (можно не обрабатывать)
        ├── RuntimeException
        │   ├── NullPointerException
        │   ├── ArrayIndexOutOfBoundsException
        │   ├── IllegalArgumentException
        │   └── custom runtime exceptions
        └── ...

Checked vs Unchecked исключения

Checked исключения — проверяются на этапе компиляции.

public void readFile(String filename) throws IOException {
    // IOException — это checked exception
    // Обязательно либо throw, либо обработать
    FileInputStream fis = new FileInputStream(filename);
    // ...
}

// При вызове ОБЯЗАТЕЛЬНО обработать
try {
    readFile("data.txt");
} catch (IOException e) {
    System.err.println("Ошибка чтения файла: " + e.getMessage());
}

Unchecked исключения — можно не обрабатывать, но лучше.

public int divide(int a, int b) {
    // ArithmeticException — unchecked
    // Компилятор не требует обработки
    return a / b;  // Может выбросить ArithmeticException если b==0
}

// Можно не обрабатывать
int result = divide(10, 2);

// Но лучше обработать
try {
    int result = divide(10, 0);
} catch (ArithmeticException e) {
    System.err.println("Деление на ноль!");
}

Основные паттерны обработки ошибок

1. Try-Catch-Finally

FileInputStream fis = null;
try {
    fis = new FileInputStream("data.txt");
    // код который может выбросить исключение
} catch (FileNotFoundException e) {
    System.err.println("Файл не найден: " + e.getMessage());
    // Обработка конкретной ошибки
} catch (IOException e) {
    System.err.println("Ошибка при чтении: " + e.getMessage());
    // Обработка более общей ошибки
} finally {
    // Выполнится в любом случае
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2. Try-with-resources (Java 7+) — РЕКОМЕНДУЕТСЯ

// Автоматически закрывает ресурсы
try (FileInputStream fis = new FileInputStream("data.txt")) {
    // код
} catch (FileNotFoundException e) {
    System.err.println("Файл не найден");
} catch (IOException e) {
    System.err.println("Ошибка чтения");
}
// fis.close() вызовется автоматически

3. Multiple resources

try (FileInputStream fis = new FileInputStream("input.txt");
     FileOutputStream fos = new FileOutputStream("output.txt")) {
    // Оба ресурса автоматически закроются
} catch (IOException e) {
    e.printStackTrace();
}

Создание собственных исключений

// Checked исключение
public class InsufficientFundsException extends Exception {
    private double amount;
    
    public InsufficientFundsException(double amount) {
        super("Недостаточно средств: " + amount);
        this.amount = amount;
    }
    
    public double getAmount() {
        return amount;
    }
}

// Unchecked исключение
public class InvalidUserException extends RuntimeException {
    public InvalidUserException(String message) {
        super(message);
    }
    
    public InvalidUserException(String message, Throwable cause) {
        super(message, cause);
    }
}

Практический пример: обработка платежей

public class PaymentService {
    private AccountRepository accountRepo;
    private PaymentLogger logger;
    
    public void processPayment(String accountId, double amount) 
            throws InsufficientFundsException, InvalidAccountException {
        
        try {
            // Проверка входных данных
            if (amount <= 0) {
                throw new IllegalArgumentException("Сумма должна быть положительной");
            }
            
            // Получение счёта
            Account account = accountRepo.findById(accountId)
                .orElseThrow(() -> new InvalidAccountException("Счёт не найден"));
            
            // Проверка баланса
            if (account.getBalance() < amount) {
                throw new InsufficientFundsException(amount);
            }
            
            // Обработка платежа
            account.setBalance(account.getBalance() - amount);
            accountRepo.save(account);
            
            // Логирование успеха
            logger.logSuccess(accountId, amount);
            
        } catch (InsufficientFundsException e) {
            logger.logError(accountId, "Недостаточно средств", e);
            throw e;  // Перебросим дальше для обработки на уровне контроллера
        } catch (InvalidAccountException e) {
            logger.logError(accountId, "Неверный счёт", e);
            throw e;
        } catch (RuntimeException e) {
            logger.logError(accountId, "Внутренняя ошибка", e);
            throw new PaymentProcessingException("Ошибка обработки платежа", e);
        }
    }
}

Обработка ошибок в многоуровневой архитектуре

Domain Layer (бизнес-логика)

public class User {
    public void setEmail(String email) {
        if (!isValidEmail(email)) {
            throw new InvalidEmailException("Неверный формат email");
        }
        this.email = email;
    }
}

Application Layer (бизнес-операции)

public class UserService {
    public void updateUserEmail(Long userId, String newEmail) 
            throws UserNotFoundException, InvalidEmailException {
        
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
        
        try {
            user.setEmail(newEmail);  // Может выбросить InvalidEmailException
            userRepository.save(user);
        } catch (InvalidEmailException e) {
            // Логируем
            logger.warn("Попытка установить неверный email для юзера " + userId);
            // Пробрасываем дальше
            throw e;
        }
    }
}

Presentation Layer (REST контроллер)

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PostMapping("/{id}/email")
    public ResponseEntity<?> updateEmail(
            @PathVariable Long id,
            @RequestBody EmailRequest request) {
        
        try {
            userService.updateUserEmail(id, request.getEmail());
            return ResponseEntity.ok().build();
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        } catch (InvalidEmailException e) {
            return ResponseEntity.badRequest()
                .body(new ErrorResponse("Invalid email: " + e.getMessage()));
        } catch (Exception e) {
            logger.error("Unexpected error", e);
            return ResponseEntity.status(500)
                .body(new ErrorResponse("Internal server error"));
        }
    }
}

Глобальный обработчик ошибок (Spring)

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(
            UserNotFoundException e) {
        return ResponseEntity.status(404)
            .body(new ErrorResponse("User not found", 404));
    }
    
    @ExceptionHandler(InvalidEmailException.class)
    public ResponseEntity<ErrorResponse> handleInvalidEmail(
            InvalidEmailException e) {
        return ResponseEntity.status(400)
            .body(new ErrorResponse("Invalid email format", 400));
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
            Exception e) {
        logger.error("Unexpected error", e);
        return ResponseEntity.status(500)
            .body(new ErrorResponse("Internal server error", 500));
    }
}

Best Practices

1. Не ловим Exception или Throwable

// ❌ ПЛОХО
try {
    // код
} catch (Exception e) {  // Слишком общее!
    // скрывает реальные проблемы
}

// ✅ ХОРОШО
try {
    // код
} catch (IOException e) {  // Конкретное исключение
    // обработка
}

2. Логируем с контекстом

// ❌ ПЛОХО
catch (IOException e) {
    System.out.println("Error occurred");
}

// ✅ ХОРОШО
catch (IOException e) {
    logger.error("Failed to read file {} for user {}", 
        filename, userId, e);
}

3. Не заедаем исключения

// ❌ ПЛОХО
try {
    processData();
} catch (Exception e) {
    // игнорируем и продолжаем
}

// ✅ ХОРОШО
try {
    processData();
} catch (DataProcessingException e) {
    logger.error("Failed to process data", e);
    throw new ApplicationException("Data processing failed", e);
}

4. Используем try-with-resources

// ❌ ПЛОХО
FileInputStream fis = new FileInputStream("file.txt");
try {
    // код
} finally {
    fis.close();  // Может выбросить исключение
}

// ✅ ХОРОШО
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // код
}  // закрытие гарантировано

5. Сохраняем stack trace

// ❌ ПЛОХО
catch (IOException e) {
    throw new RuntimeException(e.getMessage());  // Потеряли stack trace
}

// ✅ ХОРОШО
catch (IOException e) {
    throw new RuntimeException("Failed to read file", e);  // Сохранили cause
}

Тестирование обработки ошибок

@Test
public void testInvalidEmailThrowsException() {
    // Arrange
    User user = new User();
    
    // Act & Assert
    assertThrows(InvalidEmailException.class, () -> {
        user.setEmail("invalid-email");
    });
}

@Test
public void testUserNotFoundReturns404() {
    // Arrange
    given(userRepository.findById(999L))
        .willReturn(Optional.empty());
    
    // Act & Assert
    assertThrows(UserNotFoundException.class, () -> {
        userService.updateUserEmail(999L, "email@example.com");
    });
}

Типичные ошибки

  1. Поглощение исключений

    // ❌ Худшее что можно сделать
    try { riskyOperation(); } catch (Exception e) { }
    
  2. Избыточная детализация

    // ❌ Слишком много catch блоков
    catch (FileNotFoundException e) { ... }
    catch (IOException e) { ... }
    catch (Exception e) { ... }
    
  3. Потеря информации

    // ❌ Потеряли причину ошибки
    throw new RuntimeException("Error");
    
    // ✅ Сохранили причину
    throw new RuntimeException("Error", originalException);
    

Заключение

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

  • Делает приложение надёжным
  • Облегчает отладку
  • Улучшает пользовательский опыт
  • Снижает затраты на поддержку

Основные принципы:

  1. Ловим конкретные исключения, не общие
  2. Логируем с контекстом для отладки
  3. Не заедаем исключения — всегда обрабатываем или пробрасываем
  4. Сохраняем cause chain — информацию о причинах
  5. Тестируем обработку ошибок — это часть требований

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