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

  1. Track stream completion and stop calling write() once halfClose() has been issued
  2. Move all writes before the single final halfClose() call
  3. Create a new BlockingClientCall (new RPC) if more writes are needed after cancellation
  4. 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

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


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