Originally published on Code Beneath.
When a Spring Boot controller method needs to handle long-running operations without blocking the servlet thread, DeferredResult is your tool. Unlike blocking requests that tie up resources for seconds or minutes, a spring boot deferredresult timeout exception handling example shows how to offload work to a background thread pool and let the HTTP request suspend until that work finishes or times out. This approach keeps your servlet container responsive while still returning a response on the original HTTP connection. The challenge is managing timeouts, exceptions, and error callbacks correctly so that clients always receive an answer, whether success or failure.
Understanding DeferredResult Basics
DeferredResult is a Spring MVC abstraction that allows a controller to return a response asynchronously. When you return a DeferredResult from a controller method, Spring immediately releases the servlet thread. Your code runs elsewhere (typically in a thread pool) and calls setResult() or setErrorResult() on the DeferredResult object to complete the HTTP response later. The browser never knows the difference: it sends a request and waits for a response, but the server can use that wait time to process other requests.
The critical difference from traditional servlet blocking is that a single servlet thread can handle many concurrent requests instead of being pinned to one request for its entire duration. If you have 20 concurrent slow requests and only 10 servlet threads, a blocking app would queue the last 10. An async app using DeferredResult can suspend all 20 and process them in parallel on a smaller number of worker threads.
Complete Working Controller with DeferredResult
Below is a fully functional Spring Boot controller that demonstrates DeferredResult with timeout handling, error callback, and background task completion:
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.Random;
@RestController
@EnableAsync
public class AsyncController {
private final LongRunningService service;
public AsyncController(LongRunningService service) {
this.service = service;
}
@GetMapping("/async-result")
public DeferredResult<String> handleAsync() {
// Create a DeferredResult with 5 second timeout
DeferredResult<String> deferredResult = new DeferredResult<>(5000L);
// Set timeout handler: called if result not set within 5 seconds
deferredResult.onTimeout(() -> {
deferredResult.setErrorResult(new TimeoutException("Request timed out after 5 seconds"));
});
// Set completion handler: called when result is finally set (success or error)
deferredResult.onCompletion(() -> {
System.out.println("Request completed");
});
// Execute long-running task asynchronously
service.processLongTask(deferredResult);
// Return immediately, servlet thread released
return deferredResult;
}
@GetMapping("/async-with-error")
public DeferredResult<String> handleAsyncWithError() {
DeferredResult<String> deferredResult = new DeferredResult<>(3000L);
deferredResult.onTimeout(() -> {
System.err.println("Timeout occurred");
deferredResult.setErrorResult("Request exceeded 3 second timeout");
});
deferredResult.onError(throwable -> {
System.err.println("Error in async processing: " + throwable.getMessage());
});
service.processWithPotentialError(deferredResult);
return deferredResult;
}
}
@Service
class LongRunningService {
private ExecutorService executor = Executors.newFixedThreadPool(4);
public void processLongTask(DeferredResult<String> deferredResult) {
executor.submit(() -> {
try {
// Simulate work
Thread.sleep(2000);
deferredResult.setResult("Task completed successfully");
} catch (InterruptedException e) {
deferredResult.setErrorResult("Task interrupted: " + e.getMessage());
}
});
}
public void processWithPotentialError(DeferredResult<String> deferredResult) {
executor.submit(() -> {
try {
// Simulate variable work duration
long delay = new Random().nextInt(5000);
Thread.sleep(delay);
if (delay > 3500) {
// Simulate failure after most of timeout has elapsed
deferredResult.setErrorResult("Task failed: processing exceeded time limit");
} else {
deferredResult.setResult("Completed in " + delay + "ms");
}
} catch (Exception e) {
deferredResult.setErrorResult(e.getMessage());
}
});
}
}
This example creates a DeferredResult with a 5-second timeout. When a request arrives at /async-result, Spring immediately returns the DeferredResult object, releasing the servlet thread. The background task runs in a separate thread pool, sleeps for 2 seconds, then calls setResult() to complete the HTTP response. The onTimeout() callback fires if setResult() or setErrorResult() are never called within 5 seconds. The onCompletion() callback always fires when the response is finally sent to the client.
Handling Errors and Exceptions
Error handling in DeferredResult requires careful coordination. There are three ways for a DeferredResult to finish: setResult() with a success value, setErrorResult() with an error object, or a timeout. The onError() callback is invoked if an exception escapes the async processing thread (not caught and wrapped in setErrorResult()). The onTimeout() callback is your chance to provide a meaningful timeout response instead of leaving the client hanging.
A common mistake is assuming setErrorResult() accepts only exceptions. It can accept any object: an error message string, an error response object, or an exception. Spring will serialize whatever you pass and return it with an HTTP 200 status (unless you configure an error view resolver). If you want HTTP 4xx or 5xx status codes, use a ResponseEntity instead:
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AsyncErrorController {
private final LongRunningService service;
public AsyncErrorController(LongRunningService service) {
this.service = service;
}
@GetMapping("/async-status-codes")
public DeferredResult<ResponseEntity<String>> handleAsyncWithStatus() {
DeferredResult<ResponseEntity<String>> deferredResult =
new DeferredResult<>(5000L);
deferredResult.onTimeout(() -> {
ResponseEntity<String> errorResponse =
ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
.body("Request timed out after 5 seconds");
deferredResult.setErrorResult(errorResponse);
});
deferredResult.onError(throwable -> {
ResponseEntity<String> errorResponse =
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error: " + throwable.getMessage());
deferredResult.setErrorResult(errorResponse);
});
service.processLongTask(result -> {
deferredResult.setResult(ResponseEntity.ok(result));
});
return deferredResult;
}
}
DeferredResult vs CompletableFuture
Spring 5+ also supports returning CompletableFuture directly from controller methods. This is often cleaner than DeferredResult because you can chain operations with thenAccept(), thenApply(), and other functional methods. However, DeferredResult gives you more explicit control over timeout and error callbacks.
| Feature | DeferredResult | CompletableFuture |
|---|---|---|
| Timeout handling | Explicit onTimeout() callback | completeOnTimeout() method |
| Error callback | onError() for uncaught exceptions | exceptionally() chain |
| Threading model | You manage the thread pool | Uses ForkJoinPool by default |
| Chaining operations | Manual, imperative style | Functional, fluent API |
| Compatibility | Spring 3+ | Spring 5+ recommended |
Here is the same endpoint using CompletableFuture:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@RestController
public class AsyncCompletableFutureController {
private final LongRunningService service;
public AsyncCompletableFutureController(LongRunningService service) {
this.service = service;
}
@GetMapping("/async-completable")
public CompletableFuture<String> handleAsyncCompletable() {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
return "Task completed successfully";
} catch (InterruptedException e) {
throw new RuntimeException("Task interrupted", e);
}
})
.orTimeout(5, TimeUnit.SECONDS)
.exceptionally(throwable -> {
if (throwable.getCause() instanceof java.util.concurrent.TimeoutException) {
return "Request timed out after 5 seconds";
}
return "Error: " + throwable.getMessage();
});
}
@GetMapping("/async-completable-status")
public CompletableFuture<ResponseEntity<String>> handleAsyncCompletableWithStatus() {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
return ResponseEntity.ok("Task completed successfully");
} catch (InterruptedException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Task interrupted: " + e.getMessage());
}
})
.orTimeout(5, TimeUnit.SECONDS)
.exceptionally(throwable -> {
if (throwable.getCause() instanceof java.util.concurrent.TimeoutException) {
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
.body("Request timed out after 5 seconds");
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error: " + throwable.getMessage());
});
}
}
CompletableFuture is less boilerplate when chaining operations, but it runs on the ForkJoinPool by default, which may not be optimal if you need fine-grained control over thread pool size or isolation. DeferredResult forces you to manage your own ExecutorService, which is more explicit and often better for production workloads.
Thread Pool Configuration and Resource Management
A subtle gotcha: DeferredResult releases the servlet thread, but you must manage the background thread pool carefully. If you create a new thread for every request without limits, you will exhaust memory. Always use a bounded ExecutorService with a fixed thread pool or a queue-based rejection policy.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ArrayBlockingQueue;
@Configuration
public class AsyncThreadPoolConfig {
@Bean
public ExecutorService asyncExecutor() {
// Bounded thread pool: 10 threads, queue up to 100 tasks
return new ThreadPoolExecutor(
10, // core threads
10, // max threads
60, // keep-alive time
java.util.concurrent.TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100) // bounded queue
);
}
}
When the queue is full and all threads are busy, the ThreadPoolExecutor will throw RejectedExecutionException. You must catch this and return an error response immediately (before the servlet thread exits), or the client will receive no response at all.
Common Mistakes
One frequent error: forgetting to call setResult() or setErrorResult() on every code path. If your async task completes but never calls one of these methods, the HTTP request will eventually timeout and the client will see a timeout error, even though the backend task succeeded. This happens easily in exception handlers that catch and log but do not re-set the DeferredResult.
Another mistake: assuming onTimeout() runs on the background thread. It actually runs on the Spring async dispatch thread. If you call setResult() inside onTimeout(), it will have no effect because the result has already timed out. The onTimeout() callback is your last chance to produce an error response.
A third gotcha: setting the timeout too low. If your timeout is 1 second but your task genuinely takes 3 seconds, you will always timeout. Measure your actual task latency and set the timeout to a value higher than the 99th percentile, plus a safety margin.
Integration with Security Frameworks
Async request handling in Spring plays well with security. Spring Security automatically propagates the SecurityContext to async threads if you configure it correctly. For token-based authentication, you may want to reference our guide on token-based security in web applications to understand how to validate authorization headers before returning the DeferredResult. The key is that the authentication must complete before you spawn the background task, because the background thread will not have access to the servlet request context.
FAQ
What is the default timeout for DeferredResult if I do not specify one?
If you do not provide a timeout value in the constructor, DeferredResult uses the servlet async timeout, which is typically 30 seconds but varies by application server. Always specify a timeout explicitly in your code to avoid surprises in production.
Can I use DeferredResult with reactive frameworks like Project Reactor or RxJava?
Technically yes, but it defeats the purpose. If you are already using a reactive framework, return the Mono or Flux directly from your controller. DeferredResult is designed for traditional thread-per-request architectures. For a comprehensive look at alternatives to DeferredResult, see our article on Spring alternatives.
Does onTimeout() prevent the response from being sent?
No, onTimeout() is called when the timeout expires, but the response is not automatically sent at that point. Inside onTimeout(), you must call setErrorResult() or setResult() to actually send the response. If you do not call one of these methods inside onTimeout(), the request will hang indefinitely.
Can I reuse the same DeferredResult object for multiple requests?
No, each HTTP request must have its own DeferredResult instance. Once you call setResult() or setErrorResult(), that DeferredResult is exhausted and cannot be reused.
How do I test a controller that returns DeferredResult?
Use MockMvc with asyncDispatch(). Call the endpoint, then call asyncDispatch() on the result to simulate the async processing completing. Example: mockMvc.perform(get("/async-result")).andExpect(request().asyncStarted()).andDo(print()).andReturn(); then MvcResult result = ... ; mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk());
What happens if I call setResult() twice?
The first call wins. Subsequent calls are ignored and may produce a warning in logs, depending on your Spring version. Always guard against this with a flag or by wrapping your background work in a try-catch that ensures setErrorResult() is called in the catch block.
Is DeferredResult the same as async servlet processing in vanilla Java?
DeferredResult is Spring's abstraction layer on top of servlet 3.0+ async request handling. Under the hood, Spring calls startAsync() on the servlet request when you return a DeferredResult. Using DeferredResult instead of raw servlet APIs gives you Spring's lifecycle callbacks and error handling, which is usually worth the small abstraction overhead.
Top comments (0)