nautechsystems/nautilus_trader · error

Invalid state trigger {self} -> {trigger}

Error message

Invalid state trigger {self} -> {trigger}

What it means

Every actor, strategy, data/execution client, and engine in NautilusTrader is a Component governed by a strict lifecycle state machine (ComponentState: PreInitialized, Ready, Starting, Running, Stopping, Stopped, Degraded, Faulting, Faulted, Disposing, Disposed, and transitional states). ComponentState::transition in crates/common/src/component.rs:368 only accepts the enumerated (state, trigger) pairs and rejects every other combination. This error means a lifecycle command (initialize/start/stop/resume/reset/degrade/fault/dispose) was issued from a state where that trigger is not defined, for example Start while already Running, or Resume while Ready.

Source

Thrown at crates/common/src/component.rs:397

            (Self::Running, ComponentTrigger::Degrade) => Self::Degrading,
            (Self::Running, ComponentTrigger::Fault) => Self::Faulting,
            (Self::Resuming, ComponentTrigger::Stop) => Self::Stopping,
            (Self::Resuming, ComponentTrigger::ResumeCompleted) => Self::Running,
            (Self::Resuming, ComponentTrigger::Fault) => Self::Faulting,
            (Self::Stopping, ComponentTrigger::StopCompleted) => Self::Stopped,
            (Self::Stopping, ComponentTrigger::Fault) => Self::Faulting,
            (Self::Stopped, ComponentTrigger::Reset) => Self::Resetting,
            (Self::Stopped, ComponentTrigger::Resume) => Self::Resuming,
            (Self::Stopped, ComponentTrigger::Dispose) => Self::Disposing,
            (Self::Stopped, ComponentTrigger::Fault) => Self::Faulting,
            (Self::Degrading, ComponentTrigger::DegradeCompleted) => Self::Degraded,
            (Self::Degraded, ComponentTrigger::Resume) => Self::Resuming,
            (Self::Degraded, ComponentTrigger::Stop) => Self::Stopping,
            (Self::Degraded, ComponentTrigger::Fault) => Self::Faulting,
            (Self::Disposing, ComponentTrigger::DisposeCompleted) => Self::Disposed,
            (Self::Disposing, ComponentTrigger::Fault) => Self::Faulting,
            (Self::Faulting, ComponentTrigger::FaultCompleted) => Self::Faulted,
            _ => anyhow::bail!("Invalid state trigger {self} -> {trigger}"),
        };
        Ok(new_state)
    }
}

thread_local! {
    static COMPONENT_REGISTRY: ComponentRegistry = ComponentRegistry::new();
}

/// Registry for storing components with runtime borrow tracking.
///
/// The registry tracks which components are currently mutably borrowed to prevent
/// multiple simultaneous mutable borrows (which would be undefined behavior).
pub struct ComponentRegistry {
    components: RefCell<AHashMap<Ustr, Rc<UnsafeCell<dyn Component>>>>,
    borrows: RefCell<AHashSet<Ustr>>,
}

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Log or inspect component.state() before issuing the lifecycle command and only send a trigger valid for that state (start only from Ready, resume only from Stopped/Degraded, reset from Ready/Stopped).
  2. Guard calls with the provided predicates: is_running(), is_stopped(), is_degraded(), is_faulted(), is_disposed(), not_running().
  3. To reuse a stopped component, call reset() (Stopped -> Resetting -> Ready) before start(); never reuse a Disposed or Faulted component, construct a new one.
  4. Audit custom on_start/on_stop handlers for internal lifecycle calls that race with the trader's own stop/start sequence.
  5. Serialize lifecycle commands through a single task or timer so triggers cannot interleave inside transitional states.

Example fix

// before
if let Err(e) = actor.start() {
    log::error!("start failed: {e}");
}

// after
use nautilus_trader::common::component::{Component, ComponentState};
if actor.state() == ComponentState::Ready {
    actor.start()?;
} else {
    log::debug!("skipped start, state = {:?}", actor.state());
}
Defensive patterns

Strategy: validation

Validate before calling

use nautilus_trader::common::component::{Component, ComponentState};

// Only start from Ready; only resume from Stopped/Degraded
if actor.state() == ComponentState::Ready {
    actor.start()?;
}
if actor.is_stopped() || actor.is_degraded() {
    actor.resume()?;
}

Type guard

fn trigger_is_valid(state: ComponentState, trigger: ComponentTrigger) -> bool {
    use ComponentState::*;
    use ComponentTrigger::*;
    matches!(
        (state, trigger),
        (PreInitialized, Initialize)
            | (Ready, Reset | Start | Dispose)
            | (Resetting, ResetCompleted)
            | (Starting, StartCompleted | Stop | Fault)
            | (Running, Stop | Degrade | Fault)
            | (Resuming, Stop | ResumeCompleted | Fault)
            | (Stopping, StopCompleted | Fault)
            | (Stopped, Reset | Resume | Dispose | Fault)
            | (Degrading, DegradeCompleted)
            | (Degraded, Resume | Stop | Fault)
            | (Disposing, DisposeCompleted | Fault)
            | (Faulting, FaultCompleted)
    )
}

Try / catch

if let Err(e) = component.start() {
    if e.to_string().contains("Invalid state trigger") {
        log::warn!(
            "lifecycle rejected: state={:?}, retry after checking is_ready",
            component.state()
        );
    } else {
        return Err(e);
    }
}

Prevention

When it happens

Trigger: Calling component.start() when the state is anything other than Ready (e.g. a second start() on a Running component); stop() on a PreInitialized or Ready component (Stop is only valid from Starting/Running/Resuming/Degraded); resume() on Running or Ready (only valid from Stopped/Degraded); reset() while Running (only valid from Ready/Stopped); dispose() while Running (only from Ready/Stopped); or issuing any non-completion trigger while inside a transitional state such as Starting, Stopping, Resetting, Degrading, Resuming, Disposing (which accept only their Completed trigger, plus Stop/Fault where listed).

Common situations: Custom actors/strategies that call self.stop() or self.degrade() inside handlers while the trader also stops them at shutdown; tests that reuse one component instance and call start() twice; calling resume() on a component that was never stopped; trying to restart a Faulted or Disposed component instead of building a new one; lifecycle commands fired concurrently from multiple timers or tasks.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21). Data as JSON: /api/errors/daa22f10be59c81e. Report an issue: GitHub.