nautechsystems/nautilus_trader · error

System command sender should be initialized by runner

Error message

System command sender should be initialized by runner

What it means

`get_system_command_sender` clones the thread-local tokio mpsc sender for SystemCommand. It panics via `.expect` if the runner (`initialize_runner`/node startup) has not installed the sender in this thread's SYSTEM_COMMAND_SENDER slot yet. The library requires the live runner to initialize this state before any other code retrieves the sender.

Source

Thrown at crates/common/src/live/runner.rs:128

/// Replaces the system event sender for the current thread.
pub fn replace_system_event_sender(sender: tokio::sync::mpsc::UnboundedSender<SystemEvent>) {
    SYSTEM_EVENT_SENDER.with(|s| {
        *s.borrow_mut() = Some(sender);
    });
}

/// Gets the thread-local system command sender.
///
/// # Panics
///
/// Panics if the sender is uninitialized.
#[must_use]
pub fn get_system_command_sender() -> tokio::sync::mpsc::UnboundedSender<SystemCommand> {
    SYSTEM_COMMAND_SENDER.with(|sender| {
        sender
            .borrow()
            .as_ref()
            .expect("System command sender should be initialized by runner")
            .clone()
    })
}

/// Attempts to get the thread-local system command sender without panicking.
///
/// Returns `None` if the sender is not initialized.
#[must_use]
pub fn try_get_system_command_sender() -> Option<tokio::sync::mpsc::UnboundedSender<SystemCommand>>
{
    SYSTEM_COMMAND_SENDER.with(|sender| sender.borrow().as_ref().cloned())
}

/// Sets the thread-local system command sender.
///
/// Can only be called once per thread.
///
/// # Panics

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Initialize the runner before any call that reaches `get_system_command_sender` (bind senders via the runner startup path)
  2. In tests, call the test/setup helper that binds a sender (as the failing tests must) before invoking the function under test
  3. Ensure the call runs on the same OS thread that initialized the runner, since the sender is thread-local
  4. If you need a non-panicking path, use the documented non-panicking variant that returns Option

Example fix

// before
let sender = get_system_command_sender();
// after
initialize_senders(); // or start the runner first
let sender = get_system_command_sender();
Defensive patterns

Strategy: validation

Validate before calling

if !runner_senders_initialized() {
    eprintln!("runner must initialize system command sender first");
    return;
}
let sender = get_system_command_sender();

Type guard

fn sender_ready() -> bool {
    try_system_command_sender().is_some()
}

Try / catch

// Rust panics are not catchable via Result; guard with the non-panicking getter
if let Some(sender) = try_get_system_command_sender() { /* use sender */ } else { init_runner(); }

Prevention

When it happens

Trigger: Calling `get_system_command_sender()` from a thread that never ran the runner's sender-binding code, or calling it before `start()` on the runner; tests that exercise helpers which fetch the sender without first calling `bind_system_command_sender`/runner startup.

Common situations: Unit tests invoking public APIs that internally fetch the sender; custom entrypoints bypassing `LiveNode` startup; spawning the call on a different OS thread than the one that initialized the runner.

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/f819d69769785426. Report an issue: GitHub.