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

  1. Restrict read() calls to a single thread per BlockingClientCall
  2. Synchronize read access externally (e.g. synchronized block or single consumer thread)
  3. 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

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


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/1561cd46f13eeed4. Report an issue: GitHub.