grpc/grpc-java · error · IllegalStateException
The message disappeared... are you reading from multiple thr
Error message
The message disappeared... are you reading from multiple threads?
What it means
BlockingClientCall.read throws IllegalStateException when no buffered value exists and closeState is null, meaning the stream has neither produced a value nor closed. Since read() expects to always see a value or a close, this indicates concurrent reads from multiple threads corrupted the single-consumer contract of this call.
Source
Thrown at stub/src/main/java/io/grpc/stub/BlockingClientCall.java:129
return read(false, endNanoTime);
}
private RespT read(boolean waitForever, long endNanoTime)
throws InterruptedException, TimeoutException, StatusException {
Predicate<BlockingClientCall<ReqT, RespT>> predicate = BlockingClientCall::skipWaitingForRead;
executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this);
RespT bufferedValue = buffer.poll();
if (logger.isLoggable(Level.FINER)) {
logger.finer("Client Blocking read had value: " + bufferedValue);
}
CloseState currentCloseState;
if (bufferedValue != null) {
call.request(1);
return bufferedValue;
} else if ((currentCloseState = closeState.get()) == null) {
throw new IllegalStateException(
"The message disappeared... are you reading from multiple threads?");
} else if (!currentCloseState.status.isOk()) {
throw currentCloseState.status.asException(currentCloseState.trailers);
} else {
return null;
}
}
boolean skipWaitingForRead() {
return closeState.get() != null || !buffer.isEmpty();
}
/**
* Wait for a value to be available from the server. If there is an
* available value, return true immediately. If the stream was closed with Status.OK, return
* false. If the stream was closed with an error status, throw a StatusException. Otherwise, wait
* for a value to be available or the stream to be closed.
*View on GitHub (pinned to 64daddc1f3)
Solutions
- Restrict read() calls to a single thread per BlockingClientCall
- Synchronize read access externally (e.g. synchronized block or single consumer thread)
- Use a non-blocking/async stub with Flowable/StreamObserver if multiple consumers are needed
Example fix
// before
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> call.read());
pool.submit(() -> call.read()); // races -> IllegalStateException
// after
while (true) { RespT r = call.read(); if (r == null) break; handle(r); } // single thread Defensive patterns
Strategy: try-catch
Try / catch
try { RespT v = call.read(); } catch (IllegalStateException e) { // multiple-reader violation: restructure to single consumer } Prevention
- One BlockingClientCall per consumer thread
- Never share the call across executors/parallel streams
- Use async stubs when multiple consumers are required
When it happens
Trigger: Calling read() concurrently from two threads on the same BlockingClientCall; interleaving read calls such that one thread consumes a value while another checks closeState before it is set.
Common situations: Sharing a blocking client call object across worker threads instead of using one call per thread; wrapping read() in a parallel stream or executor without synchronization.
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
- Metric with name ${name} already exists
- ScheduledExecutorService not set in Builder
- ChannelLogger is not set in Builder
- NameResolverRegistry is not set in Builder
- No method bound for descriptor entry ${fullMethodName}
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/1561cd46f13eeed4.
Report an issue: GitHub.