tokio-rs/tokio · critical

A Tokio 1.x context was found, but IO is disabled. Call `ena

Error message

A Tokio 1.x context was found, but IO is disabled. Call `enable_io` on the runtime builder to enable IO.

What it means

Driver::io() calls .expect(...) when self.io is None. It panics because the runtime was built with the I/O driver disabled (no enable_io / not enable_all), yet code requested I/O driver access (tokio::net, AsyncFd, etc.).

Source

Thrown at tokio/src/runtime/driver.rs:94

    }
}

impl Handle {
    pub(crate) fn unpark(&self) {
        #[cfg(feature = "time")]
        if let Some(handle) = &self.time {
            handle.unpark();
        }

        self.io.unpark();
    }

    cfg_io_driver! {
        #[track_caller]
        pub(crate) fn io(&self) -> &crate::runtime::io::Handle {
            self.io
                .as_ref()
                .expect("A Tokio 1.x context was found, but IO is disabled. Call `enable_io` on the runtime builder to enable IO.")
        }
    }

    cfg_signal_internal_and_unix! {
        #[track_caller]
        pub(crate) fn signal(&self) -> &crate::runtime::signal::Handle {
            self.signal
                .as_ref()
                .expect("A Tokio 1.x context was found, but IO is disabled. Call `enable_io` on the runtime builder to enable IO.")
        }
    }

    cfg_time! {
        /// Returns a reference to the time driver handle.
        ///
        /// Panics if no time driver is present.
        #[track_caller]
        pub(crate) fn time(&self) -> &crate::runtime::time::Handle {

View on GitHub (pinned to 7d0d729d8f)

Solutions

  1. Call .enable_all() on the Builder (or .enable_io() explicitly).
  2. If using #[tokio::main], the default flavor already enables IO — don't pass a custom Builder that drops it.
  3. Audit all Builder chains for enable_io/enable_time/enable_signal.
  4. Isolate pure-compute tasks on a time-only runtime and run net code on a full runtime.

Example fix

// before
let rt = Builder::new_current_thread().enable_time().build()?;
rt.block_on(async { TcpStream::connect(...).await.unwrap(); }); // panics
// after
let rt = Builder::new_current_thread().enable_all().build()?;
rt.block_on(async { TcpStream::connect(...).await.unwrap(); });
Defensive patterns

Strategy: validation

Validate before calling

// Validate Builder configuration at construction time:
let rt = tokio::runtime::Builder::new_current_thread()
    .enable_all() // ensures IO driver is on
    .build()?;

Try / catch

// Panic cannot be caught across the macro; fix the Builder. If you must,
// catch_unwind around the runtime entry:
std::panic::catch_unwind(|| {
    rt.block_on(async { TcpStream::connect(...).await })
})

Prevention

When it happens

Trigger: Using TcpStream/UnixListener/AsyncFd on a runtime built via Builder without enable_io(); e.g. Builder::new_current_thread().enable_time().build() then performing networking.

Common situations: Custom Builder config forgetting enable_all; intentionally disabling IO for a CPU-only runtime then accidentally using a net API; test runtime missing IO.

Related errors


AI-assisted analysis of tokio-rs/tokio@7d0d729d8f (2026-08-11). Data as JSON: /api/errors/04b44addcc75edbc. Report an issue: GitHub.