Back to Java
2026-03-046 min read

Async Await (Java)

Learn Async Await (Java) step by step with clear examples and exercises.

Why This Matters

Asynchronous programming is crucial in modern Java applications for handling I/O-bound tasks, improving overall performance, and providing a more responsive user experience. The async and await keywords, introduced in Java 12, provide a more elegant way to write asynchronous code compared to traditional callbacks or CompletableFuture APIs. By using async-await syntax, developers can write cleaner, easier-to-read, and more maintainable code.

Prerequisites

Before diving into async-await, you should be familiar with:

  • Basic Java syntax and object-oriented programming concepts
  • Concurrency in Java (Threads, Executors, Callable, Runnable)
  • Exception handling in Java (try-catch blocks, checked exceptions, unchecked exceptions)
  • Understanding of CompletableFuture API
  • Familiarity with Spring Framework is recommended but not required for this lesson.

Core Concept

Async Methods

Async methods are declared using the @Async annotation. This tells Spring to execute the method asynchronously on a separate thread. In non-Spring applications, you can use the async keyword instead of @Async.

import org.springframework.scheduling.annotation.Async;

public class MyService {
@Async
public void doAsyncTask() {
// Asynchronous code here
}
}

In non-Spring applications:

public class MyService {
public async void doAsyncTask() {
// Asynchronous code here
}
}

Await Method

The @Async annotation works well for long-running tasks, but it doesn't help if we want to wait for the result of that task. For this purpose, Java provides the CompletableFuture#thenAccept() method, which can be used with async methods and allows us to handle the result when it becomes available.

import org.springframework.scheduling.annotation.Async;
import java.util.concurrent.CompletableFuture;

public class MyService {
@Async
public CompletableFuture<String> doAsyncTask() {
// Asynchronous code here
return CompletableFuture.completedFuture("Result");
}

public void awaitAndHandleResult() {
doAsyncTask().thenAccept(result -> {
System.out.println("Received result: " + result);
});
}
}

Async-Await Keyword (Java 17)

Java 17 introduces the async-await syntax to simplify working with asynchronous methods even further. The async keyword is used to declare an asynchronous method, and await can be used to wait for its result.

import org.springframework.stereotype.Service;

@Service
public class MyService {
public async void doAsyncTask() {
// Asynchronous code here
}

public String awaitAndHandleResult() {
CompletableFuture<String> future = this.doAsyncTask();
return future.get();
}
}

Worked Example

Let's create a simple example where we fetch data from an API asynchronously using async-await syntax.

First, add the required dependencies to your pom.xml.

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>

Next, create a service class with an async method that fetches data from an API and returns the result as a JSON object.

import org.springframework.stereotype.Service;
import org.json.JSONObject;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

@Service
public class ApiService {
public async CompletableFuture<JSONObject> fetchDataAsync() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(
new URI("https://jsonplaceholder.typicode.com/todos/1"))
.build();

return CompletableFuture.supplyAsync(() -> {
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return new JSONObject(response.body());
} catch (Exception e) {
throw new RuntimeException("Error fetching data", e);
}
});
}
}

Now, create a controller class that uses the async-await syntax to fetch and display the data.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.stereotype.Service;

@RestController
public class MainController {
private final ApiService apiService;

public MainController(ApiService apiService) {
this.apiService = apiService;
}

@GetMapping("/data")
public String fetchData() {
CompletableFuture<JSONObject> future = apiService.fetchDataAsync();
JSONObject data = future.get();
return data.toString();
}
}

Finally, run your application and access the /data endpoint to see the fetched data.

Common Mistakes

  1. Not using async keyword: For a method to be asynchronous, it must be declared with the async keyword in non-Spring applications or annotated with @Async in Spring applications.
  2. Calling await on a non-CompletableFuture: The await keyword can only be used with CompletableFutures.
  3. Forgetting to handle exceptions: Asynchronous code may run on a different thread, so it's essential to handle exceptions properly.
  4. Not waiting for the result: If you don't wait for the result of an asynchronous method, your program might continue executing without it, leading to unexpected behavior.
  5. Misusing CompletableFuture methods: Be aware that some CompletableFuture methods (e.g., join()) block the current thread and should be used with caution.
  6. Ignoring thread safety: Since asynchronous code might run on different threads, always ensure your data structures are thread-safe or properly synchronized.
  7. Not using proper exception handling: In case of exceptions, use appropriate exception handling mechanisms like try-catch blocks or CompletableFuture#handle() to handle errors gracefully.
  8. Not canceling long-running tasks: If a task takes too long or is no longer needed, it's essential to provide an option to cancel the task to avoid resource exhaustion.
  9. Not considering timeouts: When working with asynchronous methods, consider setting timeouts to prevent indefinite waiting for results.
  10. Overusing async-await: While async-await simplifies asynchronous code, it's essential to understand when to use synchronous and asynchronous methods for optimal performance and readability.

Practice Questions

  1. Write an async method that reads a file and returns its content as a string.
  2. Implement a simple web server using async-await syntax that listens for incoming requests on port 8080.
  3. Create a service class with multiple async methods, each handling a different API call. Use the CompletableFuture#allOf() method to wait for all of them to complete before continuing.
  4. Write an example using CompletableFuture#thenApply() and CompletableFuture#thenAccept() to process and print the result of an asynchronous operation.
  5. Implement a simple cache system that stores results from async operations and returns cached results if available, otherwise executes the operation asynchronously.
  6. Write an example using CompletableFuture#whenComplete() to handle exceptions and perform cleanup when an asynchronous operation completes.
  7. Create a service class with multiple async methods that can be executed in parallel using CompletableFuture#parallel().
  8. Implement a rate limiter for API calls using CompletableFutures to control the frequency of requests.
  9. Write an example using CompletableFuture#supplyAsync() and CompletableFuture#thenCombine() to combine the results of multiple asynchronous operations.
  10. Implement a simple load balancer that distributes incoming requests across multiple API endpoints using async-await syntax.

FAQ

  1. Can I use async-await in non-Spring applications? Yes, async-await is part of Java 17's standard library and can be used in any application that targets Java 17 or higher.
  2. What happens if an exception occurs in an asynchronous method? Exceptions are handled using the CompletableFuture#handle() method, which allows you to specify a function to handle exceptions when they occur.
  3. Is it possible to cancel an asynchronous task? Yes, CompletableFutures can be cancelled using the cancel() method. However, this might not always stop the underlying task immediately due to its asynchronous nature.
  4. How do I handle timeouts in async operations? You can use the CompletableFuture#whenComplete() method to specify a timeout and perform cleanup or error handling when the timeout occurs.
  5. Can I combine multiple CompletableFutures using Java streams? Yes, you can use the Stream.parallel() method to create a parallel stream of CompletableFutures and process their results using standard stream operations like map(), flatMap(), or reduce().
  6. How do I test asynchronous code effectively? Use mocking frameworks like Mockito to mock dependencies and simulate asynchronous behavior during testing. Also, consider using tools like JUnit 5's Testing Framework for Asynchronous Java Applications (TFAJA) for more comprehensive testing of asynchronous code.
  7. What are some best practices for writing asynchronous code in Java? Keep your methods small and focused, use proper exception handling, consider using a library like Project Reactor for more advanced reactive programming features, and always ensure thread safety when working with shared resources.
Async Await (Java) | Java | XQA Learn