grpc/grpc-java · error · IllegalStateException
Writes cannot be done after calling halfClose or cancel
Error message
Writes cannot be done after calling halfClose or cancel
What it means
BlockingClientCall.write throws IllegalStateException if writeClosed is true. writeClosed is set when halfClose() or cancel() was already called, after which the client half of the stream is closed and further writes are invalid per gRPC stream semantics.
Source
Thrown at stub/src/main/java/io/grpc/stub/BlockingClientCall.java:221
*
* @param request Message to send to the server
* @param timeout How long to wait before giving up. Values <= 0 are no wait
* @param unit A TimeUnit determining how to interpret the timeout parameter
* @return true if the request is sent to stream, false if skipped
* @throws TimeoutException if write does not become ready before the specified timeout expires
* @throws StatusException If the stream has closed in an error state
*/
public boolean write(ReqT request, long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException, StatusException {
long endNanoTime = System.nanoTime() + unit.toNanos(timeout);
return write(false, request, endNanoTime);
}
private boolean write(boolean waitForever, ReqT request, long endNanoTime)
throws InterruptedException, TimeoutException, StatusException {
if (writeClosed) {
throw new IllegalStateException("Writes cannot be done after calling halfClose or cancel");
}
Predicate<BlockingClientCall<ReqT, RespT>> predicate =
(x) -> x.call.isReady() || x.closeState.get() != null;
executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this);
CloseState savedCloseState = closeState.get();
if (savedCloseState == null) {
call.sendMessage(request);
return true;
} else if (savedCloseState.status.isOk()) {
return false;
} else {
throw savedCloseState.status.asException(savedCloseState.trailers);
}
}
void sendSingleRequest(ReqT request) {
call.sendMessage(request);View on GitHub (pinned to 64daddc1f3)
Solutions
- Track stream completion and stop calling write() once halfClose() has been issued
- Move all writes before the single final halfClose() call
- Create a new BlockingClientCall (new RPC) if more writes are needed after cancellation
- Guard writes with a try/catch on IllegalStateException in cleanup code
Example fix
// before call.halfClose(); call.write(req); // throws // after call.write(req); call.halfClose(); // last write first, then half-close
Defensive patterns
Strategy: try-catch
Try / catch
try { call.write(req); } catch (IllegalStateException e) { // stream already closed; drop or open new RPC } Prevention
- Issue halfClose only after all writes
- Track cancelled/half-closed state in producer loops
- Open a new RPC after cancellation instead of reusing
When it happens
Trigger: Calling write() after halfClose(); calling write() after cancel(); reusing a finished request stream in a loop that keeps sending.
Common situations: Producer code that sends messages after signaling end-of-stream; cancel-on-timeout paths followed by a final write; retry logic resending after the stream was closed.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Framer already closed
- halfClose cannot be called after already half closed or canc
- Metric with name ${name} already exists
- ScheduledExecutorService not set in Builder
- ChannelLogger is not set in Builder
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/1fb63d15bbeca76b.
Report an issue: GitHub.