tokio-rs/tokio · error · io::Error
background task failed
Error message
background task failed
What it means
Runtime error from `File::poll_write` (file.rs:781). The blocking write is dispatched via `spawn_mandatory_blocking`; if that returns `None` the runtime could not spawn the mandatory blocking task (runtime is shutting down or its blocking pool cannot accept it), and the write fails with `io::ErrorKind::Other`. The file restores a valid `Idle` state before returning.
Source
Thrown at tokio/src/fs/file.rs:781
let seek = if !buf.is_empty() {
Some(SeekFrom::Current(buf.discard_read()))
} else {
None
};
let n = buf.copy_from(src, me.max_buf_size);
let std = me.std.clone();
let res = spawn_mandatory_blocking(move || {
let res = if let Some(seek) = seek {
(&*std).seek(seek).and_then(|_| buf.write_to(&mut &*std))
} else {
buf.write_to(&mut &*std)
};
(Operation::Write(res), buf)
})
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "background task failed"));
if res.is_err() {
// Restore a valid Idle state before returning the error.
inner.state = State::Idle(Some(Buf::with_capacity(0)));
}
let blocking_task_join_handle = res?;
inner.state = State::Busy(blocking_task_join_handle);
return Poll::Ready(Ok(n));
}
State::Busy(ref mut rx) => {
let res = ready!(Pin::new(rx).poll(cx));
if res.is_err() {
// Restore a valid Idle state before returning the error.
inner.state = State::Idle(Some(Buf::with_capacity(0)));
}
let (op, buf) = res?;View on GitHub (pinned to 625954f365)
Solutions
- Ensure all `tokio::fs` I/O completes (is awaited) before the runtime is shut down.
- Keep file operations inside a live runtime (`#[tokio::main]` or a manually driven runtime that outlives them).
- Do not retain `File` handles across runtime teardown/recreation.
- Handle `io::ErrorKind::Other` as a likely terminal/shutdown condition and propagate.
Example fix
// before: file outlives the runtime
let file = tokio::fs::File::open("p").await?;
// ... runtime is dropped, then later:
file.write(&buf).await?; // 'background task failed'
// after: keep the runtime alive for the whole file lifecycle
async fn run() -> io::Result<()> {
let mut file = tokio::fs::File::create("p").await?;
file.write_all(&buf).await?;
file.flush().await?;
Ok(())
} // all I/O resolves inside the live runtime Defensive patterns
Strategy: try-catch
Type guard
fn is_bg_task_failed(e: &io::Error) -> bool {
e.kind() == io::ErrorKind::Other && e.to_string() == "background task failed"
} Try / catch
if let Err(e) = file.write(&buf).await {
if is_bg_task_failed(&e) { return Err(anyhow!("runtime unavailable for file write (shutting down?)")); }
return Err(e.into());
} Prevention
- Keep all `tokio::fs` I/O inside a live runtime and await it before shutdown.
- Do not let `File` handles outlive their runtime.
- Model `background task failed` as terminal for that file/session.
When it happens
Trigger: Issuing a `File` write while the Tokio runtime is shutting down, after the runtime context is gone, or when the mandatory blocking pool refuses the spawn. Occurs on the scalar `poll_write` path.
Common situations: Storing a `File` across runtime restarts; writing from a thread/task that outlives the runtime; dropping the runtime while file I/O is in flight; running `tokio::fs` operations outside any Tokio runtime.
Related errors
- background task failed
- other file operation is pending, call poll_complete before s
- bytes remaining on stream
- failed to write frame to transport
- frame size too big
AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11).
Data as JSON: /api/errors/1ef1c3c727e2f3dc.
Report an issue: GitHub.