How do I run coroutines in parallel using async and await in Kotlin?

In Kotlin, use async to start concurrent work inside a coroutine scope, then call await() to get each result.

Basic example:

import kotlinx.coroutines.*

suspend fun fetchUser(): String {
    delay(1000)
    return "User"
}

suspend fun fetchPosts(): List<String> {
    delay(1000)
    return listOf("Post 1", "Post 2")
}

fun main() = runBlocking {
    val userDeferred = async {
        fetchUser()
    }

    val postsDeferred = async {
        fetchPosts()
    }

    val user = userDeferred.await()
    val posts = postsDeferred.await()

    println(user)
    println(posts)
}

Here, fetchUser() and fetchPosts() run in parallel. Since both delay for 1 second, the total time is about 1 second instead of 2.

You can also await multiple results with awaitAll:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferredResults = listOf(
        async {
            delay(1000)
            "Result 1"
        },
        async {
            delay(1000)
            "Result 2"
        },
        async {
            delay(1000)
            "Result 3"
        }
    )

    val results = deferredResults.awaitAll()

    println(results)
}

Output:

[Result 1, Result 2, Result 3]

A common pattern is:

coroutineScope {
    val a = async { doWorkA() }
    val b = async { doWorkB() }

    val resultA = a.await()
    val resultB = b.await()

    combine(resultA, resultB)
}

For example:

import kotlinx.coroutines.*

suspend fun loadName(): String {
    delay(500)
    return "Alice"
}

suspend fun loadAge(): Int {
    delay(500)
    return 30
}

suspend fun loadProfile(): String = coroutineScope {
    val name = async { loadName() }
    val age = async { loadAge() }

    "${name.await()} is ${age.await()} years old"
}

fun main() = runBlocking {
    println(loadProfile())
}

Important notes:

  • async returns a Deferred<T>.
  • await() suspends until the result is ready.
  • async should usually be called inside coroutineScope, supervisorScope, runBlocking, or another coroutine.
  • If one async child fails inside coroutineScope, the whole scope is cancelled.
  • Use async for concurrent computations that return values.
  • Use launch for concurrent work that does not return a value.

If you are doing blocking I/O, use an appropriate dispatcher:

val data = async(Dispatchers.IO) {
    blockingNetworkCall()
}

For CPU-heavy work:

val result = async(Dispatchers.Default) {
    heavyCalculation()
}

How do I send async HTTP requests with HttpClient?

To send asynchronous HTTP requests in Java using the java.net.http.HttpClient, you use the sendAsync() method. This method returns a CompletableFuture<HttpResponse<T>>, allowing you to handle the response without blocking the main thread.

Here is a step-by-step example of how to implement this:

1. Basic Asynchronous GET Request

This example demonstrates how to fire a request and handle the result using thenAccept.

package org.kodejava.httpclient;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class AsyncRequestExample {
    public static void main(String[] args) {
        // 1. Create the HttpClient
        HttpClient client = HttpClient.newHttpClient();

        // 2. Build the HttpRequest
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
                .GET()
                .build();

        // 3. Send the request asynchronously
        CompletableFuture<HttpResponse<String>> responseFuture =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // 4. Handle the response when it arrives
        responseFuture.thenAccept(response -> {
            System.out.println("Status Code: " + response.statusCode());
            System.out.println("Response Body: " + response.body());
        }).exceptionally(ex -> {
            System.err.println("Error occurred: " + ex.getMessage());
            return null;
        });

        // The program continues here immediately while the request is in flight
        System.out.println("Request sent! Doing other things...");

        // Optional: Block if you need to wait for the result before the program exits
        responseFuture.join();
    }
}

2. Chaining and Transforming Results

Because sendAsync returns a CompletableFuture, you can chain operations like extracting the body or converting JSON.

CompletableFuture<String> bodyFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body)       // Transform response to just the body
        .thenApply(String::toUpperCase);      // Further transform the string

bodyFuture.thenAccept(System.out::println);

Key Components

  • sendAsync(request, bodyHandler): The non-blocking counterpart to send().
  • HttpResponse.BodyHandlers: Defines how to handle the incoming data (e.g., ofString(), ofByteArray(), or ofFile()).
  • CompletableFuture: Provides methods like .thenApply() (map), .thenAccept() (consume), and .exceptionally() (error handling).

Best Practices

  • Reuse the Client: Don’t create a new HttpClient for every request. It’s designed to be long-lived and shared.
  • Executor Service: By default, HttpClient uses a default executor. For high-load applications, you can provide your own thread pool when building the client:
    HttpClient client = HttpClient.newBuilder()
                .executor(Executors.newFixedThreadPool(10))
                .build();
    
  • Join/Get: In a console application, use .join() or .get() at the very end to prevent the main method from finishing (and the JVM exiting) before the background thread completes.