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 або 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 або спільний обробник.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ