nautechsystems/nautilus_trader · critical

Cannot take ownership - other references exist

Error message

Cannot take ownership - other references exist

What it means

Inside BybitWebSocketClient::stream(), the receiver is stored in an Arc<Receiver>; Arc::try_unwrap converts it to an owned Receiver only if this is the last reference. The expect fires when other clones of the Arc still exist (e.g. a spawn kept a handle for subscribe/order-response routing), so ownership cannot be moved into the async_stream generator.

Source

Thrown at crates/adapters/bybit/src/websocket/client.rs:951

        let cmd = HandlerCommand::Unsubscribe { topics: payloads };
        if let Err(e) = self.cmd_tx.read().await.send(cmd) {
            log::debug!("Failed to send unsubscribe command: error={e}");
        }

        Ok(())
    }

    /// Returns a stream of venue-typed [`BybitWsMessage`] items.
    ///
    /// # Panics
    ///
    /// Panics if called before [`Self::connect`] or if the stream has already been taken.
    pub fn stream(&mut self) -> impl futures_util::Stream<Item = BybitWsMessage> + use<> {
        let rx = self
            .out_rx
            .take()
            .expect("Stream receiver already taken or client not connected");
        let mut rx = Arc::try_unwrap(rx).expect("Cannot take ownership - other references exist");
        async_stream::stream! {
            while let Some(msg) = rx.recv().await {
                yield msg;
            }
        }
    }

    /// Returns the number of currently registered subscriptions.
    #[must_use]
    pub fn subscription_count(&self) -> usize {
        self.subscriptions.len()
    }

    /// Returns the credential associated with this client, if any.
    #[must_use]
    pub fn credential(&self) -> Option<&Credential> {
        self.credential.as_ref()
    }

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Drop or join all other holders of the receiver Arc before calling stream() (e.g. abort the connection task that cloned it).
  2. Restructure so the stream is the sole consumer and other components send messages into the client instead of cloning the receiver.
  3. If sharing is genuinely required, wrap messages in an Arc and use a broadcast channel rather than relying on try_unwrap.
  4. In tests, clone the Arc only after stream() has been called, or use try_recv on the owned receiver.

Example fix

// before
let rx_clone = std::mem::discriminant(&client); // some task still holds out_rx Arc
let stream = client.stream(); // panics: other Arc references exist
// after
drop(reader_task_handle); // or reader_task_handle.await; release the clone first
let stream = client.stream();
Defensive patterns

Strategy: validation

Validate before calling

// Ensure no clones of the receiver exist before taking the stream:
// join or abort tasks that cloned out_rx, then call stream().
assert!(reader_task.is_finished(), "receiver holder must be done before stream()");

Try / catch

// Unreachable-safety net only:
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| client.stream()))

Prevention

When it happens

Trigger: Calling stream() while some component (connection task, test harness, subscription router) still holds a clone of the out_rx Arc.

Common situations: Tests like test_trade_order_response_preserves_request_id that clone the receiver to assert on responses; refactors that retain an Arc handle for sending requests while also draining via stream(); failing to shut down the reader task before taking the stream.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/9294b30f33b55b9b. Report an issue: GitHub.