CompletableFuture, обработка ошибок и завершения асинхронных задач

Последнее обновление: 29.10.2025

Обработка ошибок

В прошлой статье был рассмотрен такой пример для работы с асинхронностью в языке Java, как тип CompletableFuture, который позволяет устанавливать коллбеки для обработки результатов асинхронной задачи. Однако что, если при вычислениях возникнет ошибка? Например, у нас есть следующая задача по вычислению факториала, где, если передаваемое число меньше 1, то генерируется ошибка:

Supplier<Integer> factorialTask = () -> {

    // при некорректных данных генерируем ошибку
    if(number < 1) throw new RuntimeException("Number must be greater than 0");
    int n = 1;
    int result = 1;
    while(n <= number)  result *= n++; 
    return result; 
};

Как мы можем обработать эту ошибку? Для обработки ошибок в API класса CompletedFuture определен ряд методов. Рассмотрим ряд из них:

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

Эти методы позволяют установить обратный вызов, который выполняется при возникновении ошибки на предыдущем этапе:

  • exceptionally(): вычисляет результат на основе ошибки, если она возникла на предыдущем этапе. То есть фактически обратный вызов производит переход Throwable -> T

  • exceptionallyCompose(): вызывает функцию для исключения, если она возникла на предыдущем этапе, и выполняет возвращенный результат - объект CompletableFuture. То есть фактически обратный вызов производит переход Throwable -> CompletableFuture<T>

Рассмотрим на примере вычисления факториала:

import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.Function;
import java.util.function.Consumer;
 
class Program{
 
    public static void main(String[] args) throws Exception{
          
        System.out.println("Main thread started...");
 
        int number = -5;  // исходное число для вычисления факториала
       
        // определяем задачу, которая вычисляет факториал
        Supplier<Integer> factorialTask = () -> {

            if(number < 1) throw new RuntimeException("Number must be greater than 0");
            int n = 1;
            int result = 1;
            while(n <= number)  result *= n++; 
            return result; 
        };
        // задача для вывода финального результата
        Consumer<Integer> printTask = result -> System.out.printf("Final result: %d\n", result);

        // задача для обработки ошибки
        Function<Throwable, Integer> printException = ex -> { 
            System.out.println(ex.getMessage());    
            throw new RuntimeException(ex.getMessage());
        };

        // определяем объект Executor, который будет выполнять задачи
        ExecutorService executor = Executors.newCachedThreadPool();
        CompletableFuture
                .supplyAsync(factorialTask, executor)
                .exceptionally(printException)
                .thenAccept(printTask);

        System.out.println("Main thread works...");
        System.out.println("Main thread finished...");
        
        executor.close();  // закрываем исполнителя
    }
}

Рассмотрим ключевые моменты. Прежде всего основная задача, которая выполняется с помощь CompletedFuture - это задача вычисления факториала factorialTask. Эта задача возвращает целое число (значение Integer)

Для выполнения задачи и вызова коллбеков, обрабатывающих ее результат, определяем следующий код:

CompletableFuture
    .supplyAsync(factorialTask, executor)
    .exceptionally(printException)
    .thenAccept(printTask);
  • supplyAsync(factorialTask, executor)

    Устанавливаем выполняемую асинхронную задачу и объект Executor, который выполняет эту задачу.

    Если передачаемое для вычисления факториала число некорректно, то генерируем исключение

    if(number < 1) throw new RuntimeException("Number must be greater than 0");
  • exceptionally(printException)

    Для обработки исключения определяем задачу printException, которая просто выводит информацию об исключении на консоль:

    Function<Throwable, Integer> printException = ex -> { 
        System.out.println(ex.getMessage());    
        throw new RuntimeException(ex.getMessage());
    };
    

    Здесь стоит отметить, что поскольку задача факториала возвращает значение типа Integer, то и эта задача должна возвращать значение типа Integer. Однако в данном случае мы ничего не возвращаем и просто повторно генерируем исключение (причем непроверяемое исключение). Повторная генерация исключения позволит избежать выполнения следующего колбека, для которого требуется результат вычисления факториала.

  • thenAccept(printTask)

    Задача printTask принимает результат - результат вычисления факториала и просто выводит его на консоль

В программе входное число является отрицательным:

int number = -5;

Соответственно при выполнении мы столкнемся с ошибкой и получим консольный вывод наподобие следующего:

Main thread started...
java.lang.RuntimeException: Number must be greater than 0
Main thread works...
Main thread finished...

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

Если же входное число было бы больше 0:

int number = 5;

то мы увидим результат работы коллбека из вызова thenAccept(printTask), а коллбек из вызова exceptionally(printException) не будет выполняться:

Main thread started...
Final result: 120
Main thread works...
Main thread finished...

Естественно это не единственно возможная схема обработки ошибок. Например, в примере выше мы генерировали повторно исключение. Но мы могли бы и сгенерировать результат, если переданное значение генерировало ошибку:

import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.Function;
import java.util.function.Consumer;
 
class Program{
 
    public static void main(String[] args) throws Exception{
 
        int age = -5;
       
        // определяем задачу, которая вычисляет факториал
        Supplier<Integer> validateAge = () -> {

            if(age > 110 || age < 1) throw new RuntimeException("Age is invalid");
            return age; 
        };
        // задача для вывода финального результата
        Consumer<Integer> printAge = result -> System.out.printf("Final age: %d\n", result);

        // задача для обработки ошибки
        Function<Throwable, Integer> printException = ex -> { 
            System.out.println(ex.getMessage());    
            return 18;  // возвращаем некоторое значение по умолчанию
        };

        // определяем объект Executor, который будет выполнять задачи
        ExecutorService executor = Executors.newCachedThreadPool();
        CompletableFuture
                .supplyAsync(validateAge, executor)
                .exceptionally(printException)
                .thenAccept(printAge);

        // закрываем исполнителя
        executor.close();  
    }
}

Здесь мы сначала проверяем возраст - значение переменной age в задаче validateAge: если оно не входит в неготорый диапазон, генерируем исключение:

Supplier<Integer> validateAge = () -> {

    if(age > 110 || age < 1) throw new RuntimeException("Age is invalid");
    return age; 
};

Если генерируется исключение, то вызывается задача printException:

Function<Throwable, Integer> printException = ex -> { 
    System.out.println(ex.getMessage());    
    return 18;  // возвращаем некоторое значение по умолчанию
};

Здесь также выводим сообщение об ошибке, но при этом возвращаем некоторое значение по умолчанию.

Таким образом при определении следующей цепочки:

CompletableFuture
    .supplyAsync(validateAge, executor)
    .exceptionally(printException)
    .thenAccept(printAge);

Если в validateAge возникает исключение, оно передается в задачу printException, которая генерирует конечный результат. И этот результа передается в задачу printAge. То есть в данном случае последний коллбек - printAge выполняется в любом случае, даже если возникает ошибка. Консольный вывод в данном случае

java.lang.RuntimeException: Age is invalid
Final age: 18

CompletionException

Исключение, которое генерируется в задачах CompletedFuture представляет тип CompletionException, которое наследуется от RuntimeException и фактически является оборткой над исключением. Таким образом, когда мы генерируем исключение

if(age > 110 || age < 1) throw new RuntimeException("Age is invalid");

В реальности в задачу для обработки исключения из вызова exceptionally() передается не сгенерированное исключения RuntimeException, а обертка над ним в виде объекта CompletionException. Поэтому в сообщении об исключении мы видим изначальный тип исключения - "java.lang.RuntimeException: Age is invalid". Чтобы обратиться к этому внутреннему исключению, мы можем использовать метод getCause(), который определен в интерфейсе Throwble:

Function<Throwable, Integer> printException = ex -> { 
    System.out.println(ex.getCause().getMessage());    
    return 18;  // возвращаем некоторое значение по умолчанию
};

Соответственно теперь при выводе на консоль не будет указываться тип исключения:

Age is invalid
Final age: 18

Завершение CompletedFuture

Асинхронные задачи объекта CompletableFuture могут завершиться двумя способами: с результатом или с неперехваченным исключением. Для обработки обоих случаев можно использовать метод whenComplete():

CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)

В качестве параметра этот метод принимает действие, которое соответствует методу accept() функционального интерфейса BiConsumer<T,U>:

void accept(T t, U u)

То есть действие ничего не возвращает и принимает два параметра, причем второй параметр должен представлять объект Throwable. В случае с CompletableFuture первый параметр будет представлять результат асинхронной задачи, а второй параметр - исключение, если оно возвникло в процессе выполнения асинхронной задачи.

Если результат был успешно вычислен, а исключения не возникло, то второй параметр равен null. Если же наоборот - при выполнении задачи возникло исключение, то первый параметр равен null

Общая схема работы коллбека, который передается в данный метод, выглядит следующим образом:

f.whenComplete((result, error) -> {

      if (error == null){  // если исключения нет
      
         обрабатывает результат result;
      }
      else {    // если возникло исключение

         обрабатываем ошибку error;
      }
});

Рассмотрим на примере вычисления факториала:

import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.BiConsumer;
 
class Program{
 
    public static void main(String[] args) throws Exception{
          
        System.out.println("Main thread started...");
 
        // определяем объект Executor, который будет выполнять задачи
        ExecutorService executor = Executors.newCachedThreadPool();
        work(executor, 5);
        work(executor, -5);
       
        
        System.out.println("Main thread finished...");

        executor.close(); 
    }

    static void work(Executor executor, int number){
        // определяем задачу, которая вычисляет факториал
        Supplier<Integer> factorialTask = () -> {

            if(number < 1) throw new RuntimeException("Number must be greater than 0. Invalid number: " + number);
            int n = 1;
            int result = 1;
            while(n <= number)  result *= n++;
            return result; 
        };

        BiConsumer<Integer, Throwable> completedTask = (result, ex) -> {

            if (ex == null)  // если нет исключения
                System.out.printf("factorial of %d is %d\n", number, result);
            else     // если возникло исключение
                System.out.println(ex.getCause().getMessage());
        };

        CompletableFuture
            .supplyAsync(factorialTask, executor)
            .whenComplete(completedTask);
    }
}

Основные действия здесь выполняются в методе work(), который принимает число для вычисления факториала и объект Executor для выполнения задач.

Сначала определяем собственно задачу для вычисления факториала:

Supplier<Integer> factorialTask = () -> {

    if(number < 1) throw new RuntimeException("Number must be greater than 0. Invalid number: " + number);
    int n = 1;
    int result = 1;
    while(n <= number)  result *= n++;
    return result; 
};

Если переданное число здесь меньше 1, то генерируем исключение.

Для обработки завершения задачи (вне зависимости успешного или неудачного) определяем отдельную задачу типа BiConsumer:

BiConsumer<Integer, Throwable> completedTask = (result, ex) -> {

    if (ex == null)  // если нет исключения
        System.out.printf("factorial of %d is %d\n", number, result);
    else     // если возникло исключение
         System.out.println(ex.getCause().getMessage());
};

Поскольку результат вычисления факториала представляет тип Integer, то параметры задачи представляют типы Integer и Throwable. В целях демонстрации задача просто выводит результат или сообщение об ошибке на консоль.

В конце делаем цепочку вызовов, в которые передаем выше определенные коллбеки:

CompletableFuture
    .supplyAsync(factorialTask, executor)
    .whenComplete(completedTask);

В методе main() вызываем метод work(), передавая в него для тестирования корркетные и некорректные значения:

ExecutorService executor = Executors.newCachedThreadPool();

work(executor, 5);
work(executor, -5);

В итоге мы получим консольный вывод типа следующего (порядок вывода сообщение не детерминирован):

Main thread started...
factorial of 5 is 120
Main thread finished...
Number must be greater than 0. Invalid number: -5
Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850
Morty Proxy This is a proxified and sanitized view of the page, visit original site.