JavaRush /Курсы /JAVA 25 SELF /Обработка ошибок в асинхронном коде: exceptionally, handl...

Обработка ошибок в асинхронном коде: exceptionally, handle

JAVA 25 SELF
55 уровень , 3 лекция
Открыта

1. Проблема: исключения в асинхронном коде

В обычном (синхронном) коде всё просто: если в методе случилось исключение, оно «выстреливает» вверх по стеку вызовов, и мы можем его поймать с помощью try-catch. Например:

try {
    int x = 1 / 0;
} catch (ArithmeticException ex) {
    System.out.println("Деление на ноль!");
}

В асинхронном коде ситуация сложнее. Когда мы запускаем задачу через CompletableFuture.supplyAsync, она выполняется в другом потоке. Если там произойдёт исключение, оно не выбросится в основной поток! Вместо этого оно «запакуется» внутрь объекта CompletableFuture, и если вы потом вызовете get() или join(), то получите это исключение в виде ExecutionException.

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    // Ой, тут ошибка!
    return 1 / 0;
});

try {
    Integer result = future.get(); // тут будет выброшено исключение!
} catch (Exception ex) {
    System.out.println("Произошла ошибка: " + ex.getMessage());
}

Но если вы не вызываете get() (что, кстати, само по себе не очень асинхронно), а строите цепочки через thenApply и другие методы, ошибка может «затеряться». Поэтому в асинхронном программировании очень важно уметь ловить и обрабатывать ошибки именно в цепочках CompletableFuture.

2. Метод exceptionally: обработка ошибок и возврат значения

Метод exceptionally позволяет вам поймать исключение, если оно возникло в предыдущих этапах цепочки, обработать его и вернуть альтернативное значение. Это как catch, только для асинхронного потока данных.

Сигнатура:

CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

Пример использования

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("Выполняем опасный расчёт...");
    if (Math.random() > 0.5) {
        throw new RuntimeException("Что-то пошло не так!");
    }
    return 42;
});

future = future.exceptionally(ex -> {
    System.out.println("Произошла ошибка: " + ex.getMessage());
    return 0; // Возвращаем "безопасное" значение
});

Пример с thenAccept

future.thenAccept(result -> System.out.println("Результат: " + result));

Вывод (примерный):

Выполняем опасный расчёт...
Произошла ошибка: Что-то пошло не так!
Результат: 0
Выполняем опасный расчёт...
Результат: 42

Важно! Метод exceptionally срабатывает только если в цепочке до него возникло необработанное исключение. Если всё прошло хорошо, он просто «пропускает» результат дальше.

3. Метод handle: универсальный обработчик результата и ошибки

Иногда нам нужно обработать и результат, и ошибку одновременно. Например, если всё хорошо — вернуть результат, если ошибка — вернуть запасной вариант или залогировать ошибку.

Сигнатура:

CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
  • Первый аргумент — результат (или null, если была ошибка),
  • Второй — исключение (или null, если всё хорошо).

Пример использования

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Случайная ошибка!");
    return 100;
});

CompletableFuture<Integer> safeFuture = future.handle((result, ex) -> {
    if (ex != null) {
        System.out.println("Обнаружена ошибка: " + ex.getMessage());
        return -1;
    }
    return result;
});

safeFuture.thenAccept(r -> System.out.println("Финальный результат: " + r));

Вывод:

Обнаружена ошибка: Случайная ошибка!
Финальный результат: -1
Финальный результат: 100

handle стоит использовать тогда, когда вы хотите действовать вне зависимости от того, чем закончилась задача — успешно или с ошибкой. Это такой универсальный обработчик итогов, который всегда вызывается и получает два аргумента: результат (если всё хорошо) и исключение (если что-то пошло не так).

Метод идеально подходит, если нужно централизованно логировать ошибки, вернуть значение по умолчанию, не оборвав цепочку, или просто аккуратно завершить асинхронный сценарий.

Пример:

CompletableFuture<Integer> future = CompletableFuture
    .supplyAsync(() -> 10 / 0) // здесь случится ошибка
    .handle((result, ex) -> {
        if (ex != null) {
            System.out.println("Ошибка: " + ex.getMessage());
            return 0; // значение по умолчанию
        }
        return result;
    });

System.out.println(future.join()); // выведет 0

В отличие от exceptionally, который реагирует только на ошибки, handle срабатывает всегда, позволяя обработать оба исхода в одном месте и сохранить плавность всей цепочки.

4. Метод whenComplete: побочные действия после завершения

Иногда нам не нужно менять результат, а просто хочется выполнить какое-то действие после завершения задачи — например, залогировать, что задача завершилась, независимо от того, успешно или с ошибкой.

Сигнатура:

CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
  • Первый аргумент — результат (или null при ошибке),
  • Второй — исключение (или null при успехе).

Пример использования

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Ошибка!");
    return 10;
});

future.whenComplete((result, ex) -> {
    if (ex != null) {
        System.out.println("Ошибка при выполнении: " + ex.getMessage());
    } else {
        System.out.println("Успешно завершено, результат: " + result);
    }
});

Важное отличие:
whenComplete не меняет результат или ошибку, а только выполняет действие. Если в whenComplete произойдёт исключение, оно будет «приклеено» к уже существующему.

Пример: логируем, но не вмешиваемся

future
    .whenComplete((res, ex) -> {
        System.out.println("Задача завершена. Ошибка? " + (ex != null));
    })
    .thenAccept(r -> System.out.println("Результат для пользователя: " + r));

5. Особенности и нюансы реализации

Best practices: как правильно обрабатывать ошибки в CompletableFuture

  • Всегда добавляйте обработку ошибок (exceptionally, handle или whenComplete) в цепочки асинхронных задач. В противном случае ошибка может остаться незамеченной, и приложение будет вести себя непредсказуемо.
  • Не используйте get() или join() в главном потоке без try-catch — это превратит асинхронный код в синхронный и может привести к блокировкам.
  • Если нужно вернуть «резервное» значение при ошибке — используйте exceptionally или handle.
  • Для побочных эффектов (логирование, уведомление пользователя) — используйте whenComplete.
  • В цепочках можно комбинировать: например, сначала обработать ошибку через exceptionally, затем залогировать через whenComplete, затем продолжить обработку результата.
  • Помните, что если ошибка не обработана, она «перетечёт» в следующий вызов get()/join() и может привести к падению приложения.

Порядок методов

  • Если вы используете exceptionally, он перехватывает только ошибки, возникшие до него в цепочке.
  • Если после exceptionally в цепочке снова случится ошибка (например, в thenApply), её нужно обрабатывать отдельно.
  • handle универсален — он всегда срабатывает, независимо от того, была ошибка или нет.

Комбинирование методов

CompletableFuture.supplyAsync(() -> {
    // ...
})
.handle((result, ex) -> {
    if (ex != null) return "Ошибка: " + ex.getMessage();
    return result;
})
.whenComplete((res, ex) -> {
    System.out.println("Задача завершилась, результат: " + res);
});

Что делать, если не обработать ошибку?

Если исключение не обработано и вы вызовете get() или join(), оно будет выброшено как ExecutionException (или CompletionException), и приложение может завершиться с ошибкой.

6. Типичные ошибки при обработке ошибок в CompletableFuture

Ошибка №1: отсутствие обработки ошибок. Если не добавить ни exceptionally, ни handle, ни whenComplete, ошибка просто «затеряется» до следующего вызова get()/join(), который может быть далеко от места возникновения.

Ошибка №2: использование get()/join() в главном потоке без try-catch. Это делает асинхронный код синхронным и может привести к блокировкам или неожиданным падениям приложения.

Ошибка №3: неверное понимание, где именно срабатывает обработчик. exceptionally ловит только ошибки до себя в цепочке. Если после него снова возникла ошибка, она не будет обработана этим методом.

Ошибка №4: обработка ошибки, но без возврата значения. В методе exceptionally или handle обязательно нужно вернуть значение, иначе следующий этап цепочки получит null (или не получит ничего).

Ошибка №5: путаница между handle и whenComplete. handle может менять результат, а whenComplete — только выполнять действие (например, логирование). Если вы хотите изменить результат — используйте handle.

Ошибка №6: дублирование логики обработки ошибок. Часто можно объединить обработку ошибок в одном месте, чтобы избежать дублирования кода — например, через централизованный handle или общий обработчик.

1
Задача
JAVA 25 SELF, 55 уровень, 3 лекция
Недоступна
Отчеты Сенсора: Гибкая Обработка Данных с Восстановлением
Отчеты Сенсора: Гибкая Обработка Данных с Восстановлением
1
Задача
JAVA 25 SELF, 55 уровень, 3 лекция
Недоступна
Мониторинг Системы: Детальное Логирование Событий
Мониторинг Системы: Детальное Логирование Событий
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ