nautechsystems/nautilus_trader · error · anyhow::Error

The latest block is empty

Error message

The latest block is empty

What it means

latest_block calls the base tendermint service's GetLatestBlock; `sdk_block` is an optional proto field that can be None if the node returns an empty/unsupported block payload. The library refuses to return an empty block to callers relying on block metadata.

Source

Thrown at crates/adapters/dydx/src/grpc/client.rs:387

        let req = GetNodeInfoRequest {};
        let info = self.base.get_node_info(req).await?.into_inner();
        Ok(info)
    }

    /// Query for the latest block.
    ///
    /// # Errors
    ///
    /// Returns an error if the query fails.
    pub async fn latest_block(&mut self) -> Result<Block, anyhow::Error> {
        let req = GetLatestBlockRequest::default();
        let latest_block = self
            .base
            .get_latest_block(req)
            .await?
            .into_inner()
            .sdk_block
            .ok_or_else(|| anyhow::anyhow!("The latest block is empty"))?;
        Ok(latest_block)
    }

    /// Query for the latest block height.
    ///
    /// # Errors
    ///
    /// Returns an error if the query fails.
    pub async fn latest_block_height(&mut self) -> Result<Height, anyhow::Error> {
        let latest_block = self.latest_block().await?;
        let header = latest_block
            .header
            .ok_or_else(|| anyhow::anyhow!("The block doesn't contain a header"))?;
        let height = Height(header.height.try_into()?);
        Ok(height)
    }

    /// Query for all perpetual markets.

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Retry after a short delay; a starting node will usually populate sdk_block once synced.
  2. Switch to a known-good, fully synced gRPC endpoint.
  3. Check proto crate versions (cometbone/tendermint-proto) match the dydx chain's CometBFT version.
  4. Fall back to polling latest_block_height from a different endpoint if this one repeatedly returns empty blocks.

Example fix

// before
let block = client.latest_block().await?;
// after: retry transient empty responses
let block = loop {
    match client.latest_block().await {
        Ok(b) => break b,
        Err(e) if e.to_string().contains("latest block is empty") => tokio::time::sleep(Duration::from_millis(500)).await,
        Err(e) => return Err(e),
    }
};
Defensive patterns

Strategy: retry

Try / catch

for _ in 0..3 {
    match client.latest_block().await {
        Ok(b) => return Ok(b),
        Err(e) if e.to_string().contains("latest block is empty") => tokio::time::sleep(Duration::from_millis(500)).await,
        Err(e) => return Err(e),
    }
}
Err(anyhow!("latest block empty after retries"))

Prevention

When it happens

Trigger: Calling latest_block (or latest_block_height) against a node that returns a response without sdk_block — typically a node not fully synced, a proxy/CDN stripping fields, or a proto version mismatch (tendermint vs cometbone field naming).

Common situations: Hitting a load-balanced RPC that routes to a lagging/starting node; version skew between the comet/tendermint proto crate and the node's runtime so `sdk_block` is populated under a different field.

Related errors


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