grpc/grpc-java · error · IOException
Unable to perform write due to unavailable sink.
Error message
Unable to perform write due to unavailable sink.
What it means
AsyncSink throws this IOException from the write thread (WriteRunnable.run) when the underlying okio sink has not been initialized yet, i.e. the transport's start() has not completed or stream creation failed before the sink was set. It signals that gRPC-okhttp cannot write frames because the transport's buffered sink is unavailable.
Source
Thrown at okhttp/src/main/java/io/grpc/okhttp/AsyncSink.java:230
transportExceptionHandler.onException(e);
}
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
transportExceptionHandler.onException(e);
}
}
});
}
private abstract class WriteRunnable implements Runnable {
@Override
public final void run() {
try {
if (sink == null) {
throw new IOException("Unable to perform write due to unavailable sink.");
}
doRun();
} catch (Exception e) {
transportExceptionHandler.onException(e);
}
}
public abstract void doRun() throws IOException;
}
private class LimitControlFramesWriter extends ForwardingFrameWriter {
public LimitControlFramesWriter(FrameWriter delegate) {
super(delegate);
}
@Override
public void ackSettings(Settings peerSettings) throws IOException {
controlFramesInWrite++;View on GitHub (pinned to 64daddc1f3)
Solutions
- Ensure the transport is started (start(listener)) and its start callback has completed before sending any RPC frames
- Check for exceptions reported earlier via transportExceptionHandler — the sink is null because startup failed; fix the root cause
- Avoid sharing streams across threads without synchronization; serialize writes through the stream's call executor
- Upgrade grpc-okhttp; older versions had races between start and write that could leave sink null
Example fix
// before stream.sendMessage(...); // called immediately after creating transport, before start completes // after transport.start(transportListener); // wait for listener.transportReady() or use ClientStream listener before writing stream.sendMessage(...);
Defensive patterns
Strategy: try-catch
Validate before calling
if (channel.isShutdown() || channel.isTerminated()) { throw new IllegalStateException("channel shut down"); } Type guard
boolean canSend(ManagedChannel ch) { return !ch.isShutdown() && !ch.isTerminated(); } Try / catch
try { stream.sendMessage(msg); } catch (IOException e) { if (e.getMessage().contains("unavailable sink")) { restartTransport(); } else { throw e; } } Prevention
- Wait for transportReady/start callback before writing
- Never write to streams from multiple threads unsynchronized
- Handle transportExceptionHandler to learn why startup failed
When it happens
Trigger: Calling ClientStream.sendFrame / writing to a stream before the transport finished starting, after a failed transport start, or concurrently with shutdown so the sink field is still null when WriteRunnable runs.
Common situations: Servers/clients behind flaky networks where connection setup fails; races between start() and the first write; calling write on a stream whose transport was torn down; bugs in transport lifecycle management.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Unable to load OkHttpChannelProvider
- closed
- TLS Provider failure
- Unknown negotiation type: ${negotiationType}
- Unable to decode private key
AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08).
Data as JSON: /api/errors/0026c2f39c89c5ca.
Report an issue: GitHub.