1. Проблема: винятки в асинхронному коді
У звичайному (синхронному) коді все просто: якщо в методі стається виняток, він «вистрілює» вгору стеком викликів, і ми можемо його перехопити за допомогою try-catch. Наприклад:
try {
int x = 1 / 0;
} catch (ArithmeticException ex) {
System.out.println("Ділення на нуль!");
}
У асинхронному коді ситуація складніша. Коли ми запускаємо задачу через CompletableFuture.supplyAsync, вона виконується в іншому потоці. Якщо там станеться виняток, він не буде викинутий в основний потік! Замість цього він «запаковується» всередину об’єкта CompletableFuture, і якщо ви потім викличете get() або join(), то отримаєте цей виняток у вигляді ExecutionException або CompletionException.
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. Особливості та нюанси реалізації
Найкращі практики: як правильно обробляти помилки в 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 або спільний обробник.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ