1. Introduction

Java’s CompletableFuture framework provides powerful asynchronous programming capabilities, facilitating the execution of tasks concurrently.

In this tutorial, we’ll delve into two essential methods offered by CompletableFuture –  runAsync() and supplyAsync(). We’ll explore their differences, use cases, and when to choose one over the other.

2. Understanding CompletableFuture, runAsync()* and *supplyAsync()

CompletableFuture is a powerful framework in Java that enables asynchronous programming, facilitating the execution of tasks concurrently without blocking the main thread. runAsync() and supplyAsync() are methods provided by the CompletableFuture class.

Before we dive into a comparison, let’s understand the individual functionalities of runAsync() and supplyAsync(). Both methods initiate asynchronous tasks, allowing us to execute code concurrently without blocking the main thread.

runAsync() is a method used to execute a task asynchronously that doesn’t produce a result. It’s suitable for fire-and-forget tasks where we want to execute code asynchronously without waiting for a result. For example, logging, sending notifications, or triggering background tasks.

On the other hand, supplyAsync() is a method used to asynchronously execute a task that produces a result. It’s ideal for tasks that require a result for further processing. For example, fetching data from a database, making an API call, or performing a computation asynchronously.

3. Input and Return

The main difference between runAsync() and supplyAsync() lies in the input they accept and the type of return value they produce.

3.1. runAsync()

The runAsync() method is employed when the asynchronous task to be executed doesn’t produce any result. It accepts a Runnable functional interface and initiates the task asynchronously. It returns a CompletableFuture and is useful for scenarios where the focus is on the completion of a task rather than obtaining a specific result.

Here’s a snippet showcasing its usage:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    // Perform non-result producing task
    System.out.println("Task executed asynchronously");
});

In this example, the runAsync() method is used to execute a non-result producing task asynchronously. The provided lambda expression encapsulates the task to be executed. Upon completion, it prints:

Task completed successfully

3.2. supplyAsync()

On the other hand, supplyAsync() is employed when the asynchronous task yields a result. It accepts a Supplier functional interface and initiates the task asynchronously. *Subsequently, it returns a CompletableFuture, where T is the type of the result produced by the task.*

Let’s illustrate this with an example:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Perform result-producing task
    return "Result of the asynchronous computation";
});

// Get the result later
String result = future.get();
System.out.println("Result: " + result);

In this example, supplyAsync() is used to execute a result-producing task asynchronously. The lambda expression within supplyAsync() represents the task that computes the result asynchronously. Upon completion, it prints the obtained result:

Result: Result of the asynchronous computation

4. Exception Handling

In this section, we’ll discuss how both methods handle exceptions.

4.1. runAsync()

When using runAsync(), exception handling is straightforward. The method doesn’t provide an explicit mechanism for handling exceptions within the asynchronous task. As a result, any exceptions thrown during the execution of the task are propagated to the calling thread when invoking the get() method on the CompletableFuture. This means that we must handle exceptions manually after calling get().

Here’s a snippet demonstrating exception handling with runAsync():

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    throw new RuntimeException("Exception occurred in asynchronous task");
});

try {
    future.get(); // Exception will be thrown here
} catch (ExecutionException ex) {
    Throwable cause = ex.getCause();
    System.out.println("Exception caught: " + cause.getMessage());
}

The exception message is then printed:

Exception caught: Exception occurred in asynchronous task

4.2. supplyAsync()

In contrast, supplyAsync() provides a more convenient way to handle exceptions. It offers an exception-handling mechanism via the exceptionally() method. This method allows us to specify a function that will be invoked if the original asynchronous task completes exceptionally. We can use this function to handle the exception and return a default value or perform any necessary cleanup operations.

Let’s look at an example that demonstrates how exception handling works with supplyAsync():

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Exception occurred in asynchronous task");
}).exceptionally(ex -> {
    // Exception handling logic
    return "Default value";
});

String result = future.join(); // Get the result or default value
System.out.println("Result: " + result);

In this example, if an exception occurs during the execution of the asynchronous task, the exceptionally() method will be invoked. This allows us to handle the exception gracefully and provide a fallback value if needed.

Instead of raising an exception, it prints:

Task completed with result: Default value

5. Execution Behavior

In this section, we’ll explore the execution behavior of CompletableFuture‘s runAsync() and supplyAsync() methods

5.1. runAsync()

When utilizing runAsync(), the task is launched instantly in a common thread pool. Its behavior mirrors that of invoking a new Thread(runnable).start(). This means that the task begins execution immediately upon invocation, without any delay or scheduling considerations.

5.2. supplyAsync()

On the other hand, supplyAsync() schedules the task in a common thread pool, potentially delaying its execution if other tasks are queued. This scheduling approach can be advantageous for resource management, as it helps prevent sudden bursts of thread creation. By queuing tasks and scheduling their execution based on the availability of threads, supplyAsync() ensures efficient resource utilization.

6. Chaining Operations

In this section, we’ll explore how CompletableFuture‘s runAsync() and supplyAsync() methods support chaining operations, highlighting their differences.

6.1. runAsync()

runAsync() method cannot be directly chained with methods like thenApply() or thenAccept() as it doesn’t produce a result. However, we can use thenRun() to execute another task after the runAsync() task completes. This method allows us to chain additional tasks for sequential execution without relying on the result of the initial task.

Below is an example showcasing the chaining operation using runAsync() and thenRun():

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("Task executed asynchronously");
});

future.thenRun(() -> {
    // Execute another task after the completion of runAsync()
    System.out.println("Another task executed after runAsync() completes");
});

In this example, we first execute a task asynchronously using runAsync(). Then, we use thenRun() to specify another task to be executed after the completion of the initial task. This allows us to chain multiple tasks sequentially, resulting in the following output:

Task executed asynchronously
Another task executed after runAsync() completes

6.2. supplyAsync()

In contrast, supplyAsync() allows chaining operations due to its return value. Since supplyAsync() produces a result, we can use methods like thenApply() to transform the result, thenAccept() to consume the result, or thenCompose() to chain further asynchronous operations. This flexibility enables us to build complex asynchronous workflows by chaining multiple tasks together.

Here’s an example illustrating the chaining operation with supplyAsync() and thenApply():

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "Result of the asynchronous computation";
});

future.thenApply(result -> {
    // Transform the result
    return result.toUpperCase();
}).thenAccept(transformedResult -> {
    // Consume the transformed result
    System.out.println("Transformed Result: " + transformedResult);
});

In this example, we first execute a task asynchronously using supplyAsync(), which produces a result. Then, we use thenApply() to transform the result and thenAccept() to consume the transformed result. This demonstrates the chaining of multiple operations with supplyAsync(), allowing for more complex asynchronous workflows.

Here’s an example of the output:

Transformed Result: RESULT OF THE ASYNCHRONOUS COMPUTATION

7. Performance

While both runAsync() and supplyAsync() execute tasks asynchronously, their performance characteristics can vary based on the nature of the tasks and the underlying execution environment.

7.1. runAsync()

Since runAsync() doesn’t produce any result, it may have a slightly better performance compared to supplyAsync(). This is because it avoids the overhead of creating a Supplier object. The absence of result handling logic can lead to faster task execution in certain scenarios.

7.2. supplyAsync()

Various factors influence the performance of supplyAsync(), including the complexity of the task, the availability of resources, and the efficiency of the underlying execution environment.

In scenarios where tasks involve complex computations or resource-intensive operations, the performance impact of using supplyAsync() may be more pronounced. However, the ability to handle results and dependencies between tasks can outweigh any potential performance overhead.

8. Use Cases

In this section, we’ll explore specific use cases for both methods.

8.1. runAsync()

The runAsync() method is particularly useful when the focus is on the completion of a task rather than obtaining a specific result. runAsync() is commonly employed in scenarios where background tasks or operations that don’t require returning a value are performed. For example, running periodic cleanup routines, logging events, or triggering notifications can all be achieved efficiently with runAsync().

8.2. supplyAsync()

In contrast to runAsync(), the supplyAsync() method is specifically useful when the completion of the task involves producing a value that may be utilized later in the application flow. One typical use case for supplyAsync() is fetching data from external sources, such as databases, APIs, or remote servers.

Additionally, supplyAsync() is suitable for executing computational tasks that generate a value as a result, such as performing complex calculations or processing input data.

9. Summary

Here’s a summary table comparing the key differences between runAsync() and supplyAsync():

Feature

runAsync()

supplyAsync()

Input

Accepts a Runnable representing a non-result task

Accepts a Supplier representing a result-producing task

Return Type

CompletableFuture

CompletableFuture (where T is the result type)

Use Case

Fire-and-forget tasks without result

Tasks requiring a result for further processing

Exception Handling

No built-in mechanism; exceptions propagate to the caller

Provides exceptionally() for graceful exception handling

Execution Behavior

Instantly launches task

Schedule tasks potentially delaying execution

Chaining Operations

Supports thenRun() for subsequent tasks

Supports methods like thenApply() for chaining tasks

Performance

May have a slightly better performance

Performance influenced by task complexity and resources

Use Cases

Background tasks, periodic routines, notifications

Data fetching, computational tasks, result-dependent tasks

10. Conclusion

In this article, we explored the runAsync() and supplyAsync() methods. We discussed their functionalities, differences, exception-handling mechanisms, execution behavior, chaining operations, performance considerations, and specific use cases.

While supplyAsync() is preferable when a result is needed, runAsync() is suitable for scenarios where the focus is solely on task completion without requiring a specific result.

As always, the source code for the examples is available over on GitHub.