nautechsystems/nautilus_trader · error
Cannot persist account with no events
Error message
Cannot persist account with no events: {} What it means
Raised by `account_last_event` when an `AccountAny` has no events, i.e. `account.last_event()` returns None. The SQL cache persists accounts by writing their latest `AccountState` event via `add_account`/`update_account`; an account with an empty event log cannot be represented in the database, so persisting it is treated as an internal invariant violation and aborted with this anyhow error.
Solutions
- Ensure every account receives its initial AccountState event (apply it) before calling add_account/update_account on the cache.
- Check account.last_event().is_some() before persisting; if None, skip persistence or defer until the first event arrives.
- Trace the account id from the error message to where it was constructed and fix the code path that failed to apply events.
- Verify reconstruction/deserialization logic isn't dropping the account's event log.
Example fix
// before
cache.add_account(&account).await?;
// after
if account.last_event().is_some() {
cache.add_account(&account).await?;
} else {
tracing::warn!("skipping persistence of event-less account {}", account.id());
} Defensive patterns
Strategy: validation
Validate before calling
if account.last_event().is_none() {
return Err(anyhow::anyhow!(
"refusing to persist account {} with no events",
account.id()
));
} Type guard
fn has_events(account: &AccountAny) -> bool {
account.last_event().is_some()
} Try / catch
match cache.add_account(&account).await {
Err(e) if e.to_string().contains("no events") => {
tracing::warn!("skipping event-less account {}: {e}", account.id());
}
result => result?,
} Prevention
- Only construct accounts through the event-generating account flow so the initial AccountState event always exists.
- Check last_event().is_some() before any cache persistence call.
- In restore/replay paths, assert the account event log is non-empty before caching.
- Log account.id() alongside persistence failures to speed up tracing the origin.
When it happens
Trigger: Calling `cache.add_account(account)` or `cache.update_account(account)` (PostgresCacheAdapter) with an account that has never had an `AccountState` event applied: a hand-constructed account object, a deserialized/reconstructed account whose event log was dropped, or an adapter that failed to emit the initial state event.
Common situations: Custom broker/adapter code that builds an `AccountAny` manually in tests and registers it with the cache before any state update; restoring accounts from an external system without replaying their initial state event; a bug in an event-generating client that never publishes the first AccountState.
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
- Account not found after cache update
- Cannot persist position with no events
- Verified finality requires decision evidence
- 16 lowercase hex bytes are valid TradeId
- Account event does not exist for account
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/fa226fde7f8a438b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/infrastructure/src/sql/cache.rs:1318
) -> anyhow::Result<()> {
let mut snapshot = if position.fill_voids.is_empty() {
PositionSnapshot::from(position, unrealized_pnl)
} else {
PositionSnapshot::from_replay_state(position, unrealized_pnl)
};
snapshot.ts_init = ts_snapshot;
self.add_position_snapshot(&snapshot)
}
fn heartbeat(&self, _timestamp: UnixNanos) -> anyhow::Result<()> {
todo!()
}
}
fn account_last_event(account: &AccountAny) -> anyhow::Result<AccountState> {
account
.last_event()
.ok_or_else(|| anyhow::anyhow!("Cannot persist account with no events: {}", account.id()))
}
fn order_initialized_event(order: &OrderAny) -> OrderInitialized {
order.init_event().clone()
}
fn position_last_event(position: &Position) -> anyhow::Result<OrderFilled> {
position
.last_event()
.ok_or_else(|| anyhow::anyhow!("Cannot persist position with no events: {}", position.id))
}
#[expect(
clippy::too_many_lines,
reason = "database command dispatch enumerates each cache query variant explicitly"
)]
async fn drain_buffer(pool: &PgPool, buffer: &mut VecDeque<DatabaseQuery>) {
for cmd in buffer.drain(..) {View on GitHub (pinned to 18893faf8b)