nautechsystems/nautilus_trader · error

Execution event sender should be initialized by runner

Error message

Execution event sender should be initialized by runner

What it means

`get_exec_event_sender` clones the thread-local tokio mpsc sender used to push ExecutionEvents from clients into the live runner. It panics with this message when EXEC_EVENT_SENDER has not been initialized by the runner on the current thread. This is an internal invariant: event-producing code (order submission, account state generation, position subscriptions) assumes the runner installed the channel.

Source

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

/// Replaces the system command sender for the current thread.
pub fn replace_system_command_sender(sender: tokio::sync::mpsc::UnboundedSender<SystemCommand>) {
    SYSTEM_COMMAND_SENDER.with(|s| {
        *s.borrow_mut() = Some(sender);
    });
}

/// Gets the thread-local execution event sender.
///
/// # Panics
///
/// Panics if the sender is uninitialized.
#[must_use]
pub fn get_exec_event_sender() -> tokio::sync::mpsc::UnboundedSender<ExecutionEvent> {
    EXEC_EVENT_SENDER.with(|sender| {
        sender
            .borrow()
            .as_ref()
            .expect("Execution event sender should be initialized by runner")
            .clone()
    })
}

/// Attempts to get the thread-local execution event sender without panicking.
///
/// Returns `None` if the sender is not initialized (e.g., in test environments).
#[must_use]
pub fn try_get_exec_event_sender() -> Option<tokio::sync::mpsc::UnboundedSender<ExecutionEvent>> {
    EXEC_EVENT_SENDER.with(|sender| sender.borrow().as_ref().cloned())
}

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

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Start the live runner (or call its sender-binding init) before any client/event-producing call
  2. In tests, bind a test exec-event channel first (the pattern used by runner tests)
  3. Keep event emission on the same OS thread where the runner initialized the thread-local
  4. Use the non-panicking getter variant if the sender may legitimately be absent

Example fix

// before
let tx = get_exec_event_sender();
// after
runner_bind_exec_sender(); // runner startup
let tx = get_exec_event_sender();
Defensive patterns

Strategy: validation

Validate before calling

if !runner_senders_initialized() {
    eprintln!("runner must initialize exec event sender first");
    return;
}
let sender = get_exec_event_sender();

Type guard

fn exec_sender_ready() -> bool {
    try_exec_event_sender().is_some()
}

Try / catch

// Guard with the non-panicking getter
if let Some(sender) = try_get_exec_event_sender() { /* emit event */ } else { init_runner(); }

Prevention

When it happens

Trigger: Calling `get_exec_event_sender()` or any wrapper (start, submit_order, submit_order_list_with_orders, generate_account_state, query_order, subscribe_positions) before the runner bound the exec-event channel on this thread.

Common situations: Bypassing `LiveNode::build()`/start; custom adapters emitting execution events in tests without binding a sender; running the emitting code on another thread than the one the runner initialized.

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