Explain Codes LogoExplain Codes Logo

Java Equivalent of C# async/await?

java
async-programming
completablefuture
rxjava
Anton ShumikhinbyAnton Shumikhin·Feb 26, 2025
TLDR

In Java, the CompletableFuture API is the closest analog you'll find to C#'s async/await. This is how Java tackles asynchronous programming and non-blocking operations. Here's a brief example:

CompletableFuture.supplyAsync(() -> "LongRunningMethod()") .thenApplyAsync(result -> "Process(" + result + ")") .thenAccept(System.out::println) .exceptionally(e -> { System.err.println(e); return null; });

The code initiates an asynchronous operation, processes its outcome, then consumes the result with System.out.println. This mirrors how async/await operates in C#. Any errors that crop up are addressed in the exceptionally block.

Why asynchronous?

Asynchronous programming in Java, like most other languages, brings along several advantages like non-blocking operations, better scalability, easy composability of operations, and improved exception handling.

The CompletableFuture breakdown

The art of chaining

With Java, you can chain multiple asynchronous tasks using methods like thenApply, thenCompose, and thenCombine that CompletableFuture provides. Here's an example:

// Just like your favorite band's farewell tour, this operation's gonna take a while. CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> { // Simulate a long-running operation Thread.sleep(5000); return "Result of the Operation"; }); completableFuture .thenApply(result -> result + " - Processed") // Backstage pass to see the result get processed .thenAccept(System.out::println); // Final encore where the result takes the stage

Thread control and executors

Pair up CompletableFuture with an ExecutorService to take control of the threading behavior:

ExecutorService executor = Executors.newFixedThreadPool(10); CompletableFuture.supplyAsync(() -> "Task with custom executor", executor) // Custom executors, because sometimes off-the-shelf just won't fit. .thenAcceptAsync(result -> System.out::println(result), executor); // Let the executor handle it. It's why we pay them the big bucks. executor.shutdown(); // Bringing down the curtain

Handling concurrent tasks

CompletableFuture.allOf can help you handle multiple tasks at once:

CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(future1, future2, future3); // The Avengers assemble! combinedFuture.get(); // And... action!

Going beyond CompletableFuture

CompletableFuture in Java is quite powerful, but it isn't an exact replica of async/await from C#. That's where these other libraries and tools come in handy:

  • RxJava: This library brings a rich set of operators for composing asynchronous and event-based programs.
  • Quasar: This library introduces fibers, a lightweight alternative to threads.
  • AsyncHttpClient and AsyncCompletionHandler: These classes are suitable for non-blocking HTTP operations.
  • JavaFlow and Continuations library: These provide an annotation-based approach to mimic the async/await syntax.

Uncharted territories

RxJava

Reactive programming deals with data streams and propagation of change. RxJava is a popular library in the Java ecosystem for reactive programming:

Observable<String> observable = ...; // Assume an Observable is created. observable .subscribeOn(Schedulers.io()) .observeOn(Schedulers.single()) .subscribe(result -> System.out.println(result));

Lightweight threading with fibers

Fibers, introduced by the library Quasar, offer a higher level of concurrency with much lower overhead as compared to traditional threads:

Fiber<Void> fiber = new Fiber<>(() -> { System.out.println("Inside Fiber"); return null; }).start();

Async I/O operations

The Java programming language provides AsynchronousFileChannel and AsynchronousSocketChannel, among others, for asynchronous I/O operations:

Path file = Paths.get("file.txt"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(file, READ, WRITE); ByteBuffer buffer = ByteBuffer.allocate(1024); // Grab a byte or 1024 channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() { public void completed(Integer result, ByteBuffer attachment) { System.out.println("Read done!"); // I guess reading isn't so hard after all. } public void failed(Throwable exc, ByteBuffer attachment) { // Handle read failure } });

Real-world examples

Async HTTP requests

Use AsyncHttpClient to execute HTTP requests asynchronously:

AsyncHttpClient client = Dsl.asyncHttpClient(); client.prepareGet("https://example.com") .execute() .toCompletableFuture() .thenApply(Response::getResponseBody) .thenAccept(System.out::println) .join(); client.close();

Lambda expressions

For brevity, we can make use of lambda expressions in Java to reduce boilerplate:

CompletableFuture.supplyAsync(() -> { return "A Lambda expression walks up to a bar. Then it turns into a pub."; }).thenAccept(joke -> System.out.println("Here's your joke: " + joke));

Make your own joke here. Make it a good one; I don't want to laugh all alone.

A look into state machines

Asynchronous operations often require managing complicated state transitions. CompletableFuture's combinators can help manage these transitions more declaratively.

GitHub repositories for reference

Learn by example with these GitHub repositories: