Async Programming
Async Programming
Introduction
In this lesson, you'll learn about async programming in Java. Coming from C#, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C#, you're familiar with async programming.
Java has its own approach to async programming, which we'll explore step by step.
The Java Way
Let's see how Java handles this concept. Here's a typical example:
CompletableFuture<String> fetchAsync() {
return CompletableFuture.supplyAsync(() ->
httpClient.send(request, BodyHandlers.ofString()).body()
);
}
// Chaining
CompletableFuture<Void> processAsync() {
return fetchAsync()
.thenCompose(data -> parseAsync(data))
.thenAccept(parsed -> System.out.println(parsed));
}Comparing to C#
Here's how you might have written similar code in C#:
async Task<string> FetchAsync() {
var result = await httpClient.GetStringAsync(url);
return result;
}
// Chaining
async Task ProcessAsync() {
var data = await FetchAsync();
var parsed = await ParseAsync(data);
Console.WriteLine(parsed);
}You may be used to different syntax or behavior.
C# async/await is built into the language; Java uses CompletableFuture API
You may be used to different syntax or behavior.
Task<T> (C#) maps to CompletableFuture<T> (Java)
You may be used to different syntax or behavior.
await in C# maps to .thenApply()/.thenCompose() chains in Java
You may be used to different syntax or behavior.
Both are non-blocking on the calling thread
You may be used to different syntax or behavior.
Java 21 virtual threads (Project Loom) bring async-like simplicity with blocking-style code
Step-by-Step Breakdown
1. Task vs CompletableFuture
C# Task<T> and Java CompletableFuture<T> both represent a future value. Creating one differs: C# uses the async keyword, Java uses supplyAsync or similar factory methods.
async Task<int> ComputeAsync() {
await Task.Delay(100);
return 42;
}CompletableFuture<Integer> computeAsync() {
return CompletableFuture.supplyAsync(() -> {
Thread.sleep(100); // checked exception — wrap it
return 42;
});
}2. Chaining Operations
await in C# suspends and resumes inline. Java uses method chains: thenApply (sync transform), thenCompose (async flatMap), thenAccept (terminal).
var data = await FetchAsync();
var parsed = await ParseAsync(data);
Display(parsed);fetchAsync()
.thenCompose(data -> parseAsync(data)) // async step
.thenAccept(parsed -> display(parsed)); // terminal3. Error Handling
C# uses try/catch around awaited code. Java chains .exceptionally() or .handle() on the CompletableFuture.
try {
var result = await FetchAsync();
} catch (HttpRequestException ex) {
Console.WriteLine($"Error: {ex.Message}");
}fetchAsync()
.exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return "fallback";
})
.thenAccept(result -> System.out.println(result));4. Virtual Threads (Java 21+)
Java 21 Project Loom introduces virtual threads. You can write blocking-style code (like C# sync code) and the JVM handles scheduling efficiently — no CompletableFuture chains needed.
// C# — async/await for non-blocking I/O
async Task<string> FetchAsync() {
return await httpClient.GetStringAsync(url);
}// Java 21+ virtual thread — blocking but scalable!
String fetch() {
// Run on a virtual thread; blocks the vthread, not OS thread
return httpClient.send(request, BodyHandlers.ofString()).body();
}
// Start on a virtual thread
Thread.ofVirtual().start(() -> {
String result = fetch();
System.out.println(result);
});Common Mistakes
When coming from C#, developers often make these mistakes:
- C# async/await is built into the language; Java uses CompletableFuture API
- Task<T> (C#) maps to CompletableFuture<T> (Java)
- await in C# maps to .thenApply()/.thenCompose() chains in Java
Key Takeaways
- Task<T> → CompletableFuture<T>; await → .thenCompose()/.thenApply()
- Use exceptionally() for error handling in chains
- Java 21 virtual threads enable blocking-style async code without callbacks
- Both models are non-blocking on the underlying OS thread