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

Как была устроена обработка исключений на прошлой работе

1.0 Junior🔥 111 комментариев
#Soft Skills и карьера

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

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

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

Как была устроена обработка исключений на прошлой работе

Контекст

На прошлой работе я разрабатывал микросервис обработки платежей (платформа e-commerce). Я помогу привести реалистичный пример, как мы структурировали обработку исключений.

Слои обработки исключений

1. Домен (Domain Layer)

Мы определили свои исключения для бизнес-логики:

// Базовое исключение для доменного слоя
public abstract class DomainException extends RuntimeException {
    public DomainException(String message) {
        super(message);
    }
    
    public DomainException(String message, Throwable cause) {
        super(message, cause);
    }
}

// Конкретные исключения
public class InsufficientFundsException extends DomainException {
    private final BigDecimal required;
    private final BigDecimal available;
    
    public InsufficientFundsException(BigDecimal required, BigDecimal available) {
        super(String.format("Insufficient funds. Required: %s, Available: %s", 
              required, available));
        this.required = required;
        this.available = available;
    }
    
    public BigDecimal getRequired() { return required; }
    public BigDecimal getAvailable() { return available; }
}

public class PaymentAlreadyProcessedException extends DomainException {
    private final String paymentId;
    
    public PaymentAlreadyProcessedException(String paymentId) {
        super("Payment " + paymentId + " has already been processed");
        this.paymentId = paymentId;
    }
}

public class InvalidTransactionStateException extends DomainException {
    private final String currentState;
    private final String attemptedAction;
    
    public InvalidTransactionStateException(String currentState, String attemptedAction) {
        super(String.format("Cannot %s transaction in %s state", 
              attemptedAction, currentState));
        this.currentState = currentState;
        this.attemptedAction = attemptedAction;
    }
}

2. Сервисный слой (Application Layer)

Сервисы выбрасывали исключения и документировали их:

@Service
@RequiredArgsConstructor
public class PaymentService {
    private final PaymentRepository repository;
    private final AccountService accountService;
    private final AuditLogger auditLogger;
    
    /**
     * Обрабатывает платёж
     * @param paymentRequest запрос на платёж
     * @return результат обработки
     * @throws InsufficientFundsException если нет средств
     * @throws PaymentAlreadyProcessedException если уже обработан
     */
    public PaymentResult processPayment(PaymentRequest paymentRequest) {
        // Логика с несколькими проверками
        Payment payment = repository.findById(paymentRequest.getPaymentId())
            .orElseThrow(() -> new PaymentNotFoundException(
                paymentRequest.getPaymentId()));
        
        // Проверка статуса
        if (payment.isProcessed()) {
            throw new PaymentAlreadyProcessedException(
                paymentRequest.getPaymentId());
        }
        
        // Проверка баланса
        Account account = accountService.getAccount(payment.getAccountId());
        if (account.getBalance().compareTo(payment.getAmount()) < 0) {
            throw new InsufficientFundsException(
                payment.getAmount(), 
                account.getBalance());
        }
        
        try {
            // Обработка платежа
            payment.process();
            repository.save(payment);
            
            // Логирование успеха
            auditLogger.log("Payment processed", payment.getId());
            
            return new PaymentResult(payment.getId(), "SUCCESS");
            
        } catch (DatabaseException ex) {
            // Пробросить выше, не обрабатывать здесь
            auditLogger.logError("DB error during payment", payment.getId(), ex);
            throw ex;
        } catch (Exception ex) {
            // Неожиданное исключение
            auditLogger.logError("Unexpected error", payment.getId(), ex);
            throw new PaymentProcessingException(
                "Failed to process payment", ex);
        }
    }
}

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

Контроллеры обрабатывали исключения и преобразовывали в HTTP ответы:

@RestController
@RequestMapping("/api/payments")
@RequiredArgsConstructor
public class PaymentController {
    private final PaymentService paymentService;
    private final PaymentMapper mapper;
    
    @PostMapping
    public ResponseEntity<?> processPayment(
            @Valid @RequestBody PaymentRequest request) {
        
        try {
            PaymentResult result = paymentService.processPayment(request);
            return ResponseEntity.status(HttpStatus.CREATED)
                .body(mapper.toResponse(result));
                
        } catch (InsufficientFundsException ex) {
            // 402 Payment Required
            ErrorResponse error = ErrorResponse.builder()
                .code("INSUFFICIENT_FUNDS")
                .message(ex.getMessage())
                .timestamp(Instant.now())
                .details(Map.of(
                    "required", ex.getRequired(),
                    "available", ex.getAvailable()
                ))
                .build();
            return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
                .body(error);
                
        } catch (PaymentAlreadyProcessedException ex) {
            // 409 Conflict
            ErrorResponse error = ErrorResponse.builder()
                .code("ALREADY_PROCESSED")
                .message(ex.getMessage())
                .timestamp(Instant.now())
                .build();
            return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(error);
                
        } catch (DomainException ex) {
            // 400 Bad Request для прочих доменных ошибок
            ErrorResponse error = ErrorResponse.builder()
                .code("VALIDATION_ERROR")
                .message(ex.getMessage())
                .timestamp(Instant.now())
                .build();
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(error);
        }
    }
}

4. Global Exception Handler

Мы использовали @ControllerAdvice для централизованной обработки:

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    // Доменные исключения (бизнес-логика)
    @ExceptionHandler(InsufficientFundsException.class)
    public ResponseEntity<ErrorResponse> handleInsufficientFunds(
            InsufficientFundsException ex, HttpServletRequest request) {
        
        log.warn("Insufficient funds error: {}", ex.getMessage());
        
        return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
            .body(ErrorResponse.builder()
                .code("INSUFFICIENT_FUNDS")
                .message(ex.getMessage())
                .path(request.getRequestURI())
                .timestamp(Instant.now())
                .build());
    }
    
    // Ошибки валидации
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex, HttpServletRequest request) {
        
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.toList());
        
        log.warn("Validation error: {}", errors);
        
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(ErrorResponse.builder()
                .code("VALIDATION_ERROR")
                .message("Input validation failed")
                .errors(errors)
                .timestamp(Instant.now())
                .path(request.getRequestURI())
                .build());
    }
    
    // Неожиданные ошибки (логируем как ошибку)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
            Exception ex, HttpServletRequest request) {
        
        String traceId = UUID.randomUUID().toString();
        log.error("Unexpected error [trace: {}]", traceId, ex);
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse.builder()
                .code("INTERNAL_ERROR")
                .message("An unexpected error occurred")
                .traceId(traceId)  // Для отслеживания в логах
                .timestamp(Instant.now())
                .path(request.getRequestURI())
                .build());
    }
}

Структура ErrorResponse

@Data
@Builder
public class ErrorResponse {
    private String code;              // Код ошибки (INSUFFICIENT_FUNDS)
    private String message;           // Сообщение об ошибке
    private Instant timestamp;        // Когда произошла ошибка
    private String path;              // Какой путь вызвал ошибку
    private String traceId;           // Для отслеживания в логах
    private List<String> errors;      // Дополнительные ошибки (валидация)
    private Map<String, Object> details; // Доп. информация
}

Логирование ошибок

@Component
@Slf4j
public class ExceptionLogger {
    
    /**
     * Логируем только то, что нужно
     */
    public void logDomainException(DomainException ex, String context) {
        // Доменное исключение — обычная ситуация (log.warn)
        log.warn("Domain error in {}: {}", context, ex.getMessage());
    }
    
    public void logTechnicalException(Exception ex, String context) {
        // Техническая ошибка (БД, сеть и т.д.) — ошибка (log.error)
        log.error("Technical error in {}: {}", context, ex.getMessage(), ex);
    }
    
    public void logUnexpectedException(Exception ex, String context, String traceId) {
        // Непредвиденная ошибка — критическая (log.error с трассировкой)
        log.error("UNEXPECTED ERROR [traceId={}] in {}", traceId, context, ex);
    }
}

Правила обработки исключений

1. **Не подавляй (don't swallow)**
   ❌ try { ... } catch (Exception ex) { } // Ошибка исчезла!
   ✅ Либо пробрось выше, либо логируй и оборачивай

2. **Логируй один раз на уровне**
   ❌ log.error(...) в методе
      ...выбрасываем исключение...
      log.error(...) в обработчике
      Логируется ДВА РАЗА!
   ✅ Выбрасываем исключение один раз, логируем один раз

3. **Используй свои исключения для бизнес-логики**
   ✅ throw new InsufficientFundsException(...)
   ❌ throw new IllegalArgumentException(...)

4. **Оборачивай низкоуровневые ошибки в свои исключения**
   ✅ } catch (SQLException ex) {
         throw new DatabaseException("Failed to fetch user", ex);
      }
   ❌ throw new RuntimeException(ex);

5. **Предоставляй контекст в исключении**
   ✅ new PaymentProcessingException(
         "Payment " + paymentId + " failed", cause);
   ❌ throw new Exception("Error");

На практике: обработка транзакций

@Transactional
public void transferMoney(String fromAccountId, String toAccountId, 
                         BigDecimal amount) {
    try {
        Account from = accountService.lockAccount(fromAccountId);
        Account to = accountService.lockAccount(toAccountId);
        
        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException(amount, from.getBalance());
        }
        
        from.debit(amount);
        to.credit(amount);
        
        accountRepository.save(from);
        accountRepository.save(to);
        
        auditLogger.log("Transfer completed", fromAccountId, toAccountId, amount);
        
    } catch (InsufficientFundsException ex) {
        // Spring откатит транзакцию автоматически (@Transactional)
        throw ex;  // Пробрасываем для обработки выше
        
    } catch (DatabaseException ex) {
        // БД ошибка — откат транзакции
        auditLogger.logError("Database error during transfer", ex);
        throw new TransferFailedException("Database error", ex);
        
    } catch (Exception ex) {
        // Неожиданная ошибка — откат и логирование
        auditLogger.logError("Unexpected error during transfer", ex);
        throw new TransferFailedException("Unexpected error", ex);
    }
}

Итого: мой подход к обработке исключений

  1. Иерархия: DomainException -> конкретные исключения (InsufficientFundsException и т.д.)
  2. Слои: доменные ошибки -> сервисный слой -> контроллер -> обработчик -> HTTP ответ
  3. Маппирование: каждое исключение -> HTTP код (400, 402, 409 и т.д.)
  4. Логирование: один раз на уровне обработки (не в каждом слое)
  5. Контекст: сохраняем информацию об ошибке (ID, сумму, состояние и т.д.)
  6. Отслеживание: traceId для отслеживания в логах

Это позволило нам:

  • Давать клиентам понятные ошибки
  • Легко отладить проблемы в продакшене
  • Разделить бизнес-логику от технических ошибок
  • Избежать дублирования кода обработки исключений
Как была устроена обработка исключений на прошлой работе | PrepBro