๐ด The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
2026-02-20 13:40:05.200 ERROR 8842 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:475)
at jakarta.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:141)โก Quick Fix Works 80% of the time
Do not write to the HttpServletResponse directly before throwing an exception.
// BAD
response.getWriter().write("Hello");
throw new RuntimeException();
// GOOD
return ResponseEntity.ok("Hello");๐ง Why this Happens
Tap to expand the deep technical explanation
An HTTP response is 'committed' when the HTTP headers (and possibly some body content) have already been sent over the network to the client. In your code, an exception occurred AFTER this point. Spring tried to intercept the exception and return a 500 Error JSON, but it cannot modify the response anymore because it was already sent.
The HITEC City Parking Spot Analogy:
It's like trying to unsend a text message after it has already been delivered and read. The network has committed the data, and you cannot take it back or change the message.
๐ How to Reproduce Confirm this is your error
In a controller method, call `response.getWriter().write("Hello"); response.getWriter().flush();`. Immediately after, `throw new RuntimeException()`. Spring will try to render the error page but fail.
๐ ๏ธ Solutions (5 Ways to Fix)
Return ResponseEntity instead of using HttpServletResponse
๐ Use this as the modern Spring MVC best practice.
Spring manages the response buffer. If you return an object or ResponseEntity, Spring waits until the method finishes successfully before committing the response.
@GetMapping("/data")
public ResponseEntity<String> getData() {
// No direct response manipulation
return ResponseEntity.ok("Hello");
}Do not flush streams manually
๐ Use this if you are manually writing to OutputStreams.
Calling `flush()` forces the server to commit the headers and send the bytes immediately. Let Spring/Tomcat handle flushing.
// Remove this line:
// response.getWriter().flush();Handle file downloads carefully
๐ Use this if you are streaming large files.
If an error occurs halfway through a streaming download, you can't send an error JSON. Wrap the stream in a try-catch and just log the error.
try (InputStream is = new FileInputStream(file)) {
StreamUtils.copy(is, response.getOutputStream());
} catch (IOException e) {
// Cannot send 500 error now, just log it
log.error("File download interrupted", e);
}Use @RestControllerAdvice for errors
๐ Use this to catch exceptions globally before they hit the servlet layer.
Global exception handlers intercept the exception before the response is committed, allowing you to return clean JSON.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleAll(Exception e) {
return ResponseEntity.status(500).body("Error");
}
}Check for forward/include loops
๐ Use this if the error occurs during view rendering (JSP/Thymeleaf).
If a view throws an error after the template engine started writing output, the response is committed. Catch errors in the controller before rendering.
// Validate data in the controller BEFORE adding to Model
if (user == null) throw new RuntimeException();
model.addAttribute("user", user);
return "view";๐ Version Notes
Uses javax.servlet.
Uses jakarta.servlet. Stricter commit rules in Tomcat 10+.
๐ก๏ธ How to Prevent This Next Time
Never use `HttpServletResponse` directly in a `@RestController`. Always return objects and let Spring handle the HTTP response buffer.