bevyengine/bevy · critical

Attempted to access or drop non-send data {} from thread {:?

Error message

Attempted to access or drop non-send data {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.

What it means

NonSend<T> resources are not Send: bevy only permits them to be accessed - and dropped - on the exact thread they were created on. This panic fires from NonSend's validate_access when the recorded origin thread id does not match std::thread::current().id(), meaning non-send data crossed threads through a path that promised it would not. In no_std (single-threaded) builds the check is skipped.

Source

Thrown at crates/bevy_ecs/src/storage/non_send.rs:78

            self.data.drop(1, self.is_present().into());
        }
    }
}

impl NonSendData {
    /// The only row in the underlying `BlobArray`.
    const ROW: usize = 0;

    /// Validates that the access to `NonSendData` is only done on the thread they were created from.
    ///
    /// # Panics
    /// This will panic if called from a different thread than the one it was inserted from.
    #[inline]
    fn validate_access(&self) {
        #[cfg(feature = "std")]
        if self.origin_thread_id != Some(std::thread::current().id()) {
            // Panic in tests, as testing for aborting is nearly impossible
            panic!(
                "Attempted to access or drop non-send data {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.",
                self.type_name,
                self.origin_thread_id,
                std::thread::current().id()
            );
        }

        // TODO: Handle no_std non-send.
        // Currently, no_std is single-threaded only, so this is safe to ignore.
        // To support no_std multithreading, an alternative will be required.
        // Remove the #[expect] attribute above when this is addressed.
    }

    /// Returns true if the data is populated.
    #[inline]
    pub fn is_present(&self) -> bool {
        self.is_present
    }

View on GitHub (pinned to 396ca72708)

Solutions

  1. Create, access, and drop the World/NonSend resource on the same thread - typically main.
  2. If the data must be touched elsewhere, send ownership to the origin thread via a channel, or restructure it into Send data (raw handle plus thread-local context).
  3. In tests, construct and tear down the App inside the same test body instead of moving it between threads.

Example fix

// before
let app = thread::spawn(|| { let mut a = App::new(); a.insert_non_send_resource(Handle::new()); a }).join().unwrap();
drop(app); // NonSend dropped on the wrong thread -> panic

// after
let mut app = App::new();
app.insert_non_send_resource(Handle::new());
// insert, access and drop all happen on this same thread
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Dropping the World (or removing the NonSend resource) on a different thread than where it was inserted; accessing NonSend data from a schedule running on worker threads; constructing an App in one thread and dropping it in another (tests, FFI hosts, embedded runtimes).

Common situations: Test harnesses that build an App inside thread::spawn and then drop it on main; engines/runtimes that relocate work between threads; window/GPU handles stored as NonSend touched after the main thread changed.

Related errors


AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20). Data as JSON: /api/errors/5c2bbd0ac4c343f3. Report an issue: GitHub.