Gymterview
middle

Как обрабатывать ошибки в реактивных потоках?

В реактивном программировании ошибка — терминальный сигнал, после которого поток прекращает работу. Project Reactor предоставляет набор операторов для перехвата, замены и восстановления после ошибок.

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

Оператор Назначение Пример
onErrorReturn Заменить ошибку значением по умолчанию flux.onErrorReturn(-1)
onErrorResume Заменить ошибку альтернативным потоком mono.onErrorResume(e -> fallback())
onErrorMap Преобразовать ошибку в другой тип mono.onErrorMap(e -> new ApiException(e))
doOnError Побочный эффект (логирование) без перехвата mono.doOnError(e -> log.error(...))
retry / retryWhen Повторная попытка подписки mono.retry(3)
onErrorComplete Преобразовать ошибку в сигнал завершения flux.onErrorComplete()
Пример
// onErrorReturn — значение по умолчанию
Flux.just(1, 2, 0)
    .map(i -> 10 / i)
    .onErrorReturn(-1)
    .subscribe(System.out::println);
// Вывод: 10, 5, -1
Пример
// onErrorResume — разная обработка для разных типов ошибок
Mono<User> user = userService.findById(id)
    .onErrorResume(NotFoundException.class, e -> Mono.empty())
    .onErrorResume(ServiceException.class, e -> fallbackService.findById(id));
Пример
// onErrorMap — преобразование типа ошибки
Mono<User> user = userRepository.findById(id)
    .onErrorMap(DataAccessException.class,
                e -> new ServiceException("Ошибка БД", e));
Пример: retryWhen с backoff
Mono<Response> response = webClient.get()
    .uri("/api/data")
    .retrieve()
    .bodyToMono(Response.class)
    .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
        .maxBackoff(Duration.ofSeconds(10))
        .filter(e -> e instanceof WebClientResponseException.ServiceUnavailable)
        .onRetryExhaustedThrow((spec, signal) ->
            new ServiceException("Сервис недоступен после 3 попыток")));
Пример: комбинированная обработка ошибок
Mono<OrderDto> result = orderService.createOrder(request)
    .doOnError(e -> log.error("Ошибка создания заказа", e))
    .onErrorMap(DataAccessException.class,
                e -> new ApiException(500, "Ошибка базы данных"))
    .onErrorMap(ValidationException.class,
                e -> new ApiException(400, e.getMessage()))
    .retryWhen(Retry.backoff(2, Duration.ofMillis(500))
        .filter(e -> e instanceof TransientDataAccessException));

Частые ошибки

  • onErrorReturn в середине цепочки — поток завершится после возврата значения, последующие элементы Flux не будут эмитированы
  • Бесконечный retry без фильтра — бесконечный цикл при постоянной ошибке
  • Retry для неидемпотентных операций — повтор POST-запроса может создать дубликаты
  • Игнорирование ошибок без doOnError — если ошибка «проглатывается», важно хотя бы залогировать

Как используется в 2026

  • retryWhen с Retry.backoff — стандартный паттерн для устойчивости микросервисов
  • Интеграция с Resilience4j для circuit breaker + retry
  • Spring WebFlux: @ExceptionHandler и WebExceptionHandler для глобальной обработки
  • Structured error handling через onErrorResume с разными типами исключений — best practice

На собеседовании: покажите знание нескольких операторов и объясните разницу: onErrorReturn заменяет значением, onErrorResume — потоком, onErrorMap — типом ошибки, doOnError — побочный эффект. Частая ошибка — не знать, что порядок операторов важен: ошибка перехватывается ближайшим оператором ниже по цепочке.