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 или общий обработчик.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ