tokio-rs/tokio · error · io::Error
write zero byte into writer
Error message
write zero byte into writer
What it means
Thrown by the copy/copy_bidirectional pump loop when poll_write_buf returns 0 bytes written while there is still buffered input to flush. Tokio signals io::ErrorKind::WriteZero because the destination cannot accept any data, so continuing would loop forever — the adjacent debug_assert even warns that a buggy poll_write returning 0 will block. It is a terminal condition for the copy future.
Source
Thrown at tokio/src/io/util/copy.rs:173
}
}
// If our buffer has some data, let's write it out!
while self.pos < self.cap {
let i = ready!(self.poll_write_buf(cx, reader.as_mut(), writer.as_mut()))?;
#[cfg(any(
feature = "fs",
feature = "io-std",
feature = "net",
feature = "process",
feature = "rt",
feature = "signal",
feature = "sync",
feature = "time",
))]
coop.made_progress();
if i == 0 {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::WriteZero,
"write zero byte into writer",
)));
} else {
self.pos += i;
self.amt += i as u64;
self.need_flush = true;
}
}
// If pos larger than cap, this loop will never stop.
// In particular, user's wrong poll_write implementation returning
// incorrect written length may lead to thread blocking.
debug_assert!(
self.pos <= self.cap,
"writer returned length larger than input slice"
);
View on GitHub (pinned to 625954f365)
Solutions
- Match on io::ErrorKind::WriteZero at the copy call site and treat it as 'destination closed' — typically tear down the bridging task.
- Ensure the destination is alive for the whole copy (don't drop/close the write half prematurely).
- If the writer is custom, return Poll::Pending (registering cx.waker()) rather than Ok(0) when not ready.
- For copy_bidirectional, half-close gracefully: on WriteZero from one direction, finish draining the other direction before exiting.
Example fix
// before
let n = tokio::io::copy(&mut reader, &mut writer).await?;
// after
match tokio::io::copy(&mut reader, &mut writer).await {
Ok(n) => Ok(n),
Err(e) if e.kind() == io::ErrorKind::WriteZero => {
// writer closed mid-copy; reader may still have data
Ok(0)
}
Err(e) => Err(e),
} Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure the destination is writable before copying by tracking its liveness. // There is no portable pre-check; the recommended guard is at the call site.
Type guard
fn is_copy_write_zero(e: &io::Error) -> bool {
e.kind() == io::ErrorKind::WriteZero
} Try / catch
match tokio::io::copy(&mut r, &mut w).await {
Ok(n) => Ok(n),
Err(e) if e.kind() == io::ErrorKind::WriteZero => {
// destination closed mid-copy; tear down the bridge
Ok(0)
}
Err(e) => Err(e.into()),
} Prevention
- Keep the destination alive for the entire copy; don't drop the write half early.
- For custom AsyncWrite on the destination, return Pending instead of Ok(0).
- In copy_bidirectional, finish draining the surviving direction on WriteZero.
- Log the bytes copied before the failure for diagnostics.
When it happens
Trigger: tokio::io::copy, copy_bidirectional, or the internal CopyBuffer driving a reader→writer transfer where the writer returns Ok(0) on a non-empty write request. Happens when the destination socket/pipe/file is closed or full-and-closed before the source is exhausted.
Common situations: Proxying between two TCP connections where the client/server closes early; piping into a process that exited (broken pipe); a writer fd that hit EOF; a custom AsyncWrite returning Ok(0) instead of Pending. The reader side still has data, but the sink is gone.
Related errors
- failed to write the buffered data
- stream did not contain valid UTF-8
- early eof
- early eof
- utf-8 error (io::Error::new(io::ErrorKind::InvalidData, err)
AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11).
Data as JSON: /api/errors/2d163807f9091c1f.
Report an issue: GitHub.