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

  1. Ensure all `tokio::fs` I/O completes (is awaited) before the runtime is shut down.
  2. Keep file operations inside a live runtime (`#[tokio::main]` or a manually driven runtime that outlives them).
  3. Do not retain `File` handles across runtime teardown/recreation.
  4. 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

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


AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11). Data as JSON: /api/errors/1ef1c3c727e2f3dc. Report an issue: GitHub.