FuelLabs/fuel-core · error · anyhow::Error

Total balance overflow: coins: {coins}, messages: {non_retry

Error message

Total balance overflow: coins: {coins}, messages: {non_retryable}

What it means

The GraphQL off-chain balance query adds the owner's base-asset coin total and non-retryable message balance with checked_add on TotalBalanceAmount (u64). Overflow requires the combined value to exceed u64::MAX — a state a sane chain cannot reach — so this is a sanity guard against corrupted storage or absurd genesis amounts, not an expected arithmetic event.

Source

Thrown at crates/fuel-core/src/service/adapters/graphql_api/off_chain.rs:225

        base_asset_id: &AssetId,
    ) -> StorageResult<TotalBalanceAmount> {
        let coins = self
            .storage_as_ref::<CoinBalances>()
            .get(&CoinBalancesKey::new(owner, asset_id))?
            .unwrap_or_default()
            .into_owned() as TotalBalanceAmount;

        if base_asset_id == asset_id {
            let MessageBalance {
                retryable: _, // TODO: https://github.com/FuelLabs/fuel-core/issues/2448
                non_retryable,
            } = self
                .storage_as_ref::<MessageBalances>()
                .get(owner)?
                .unwrap_or_default()
                .into_owned();

            let total = coins.checked_add(non_retryable).ok_or(anyhow::anyhow!(
                "Total balance overflow: coins: {coins}, messages: {non_retryable}"
            ))?;
            Ok(total)
        } else {
            Ok(coins)
        }
    }

    fn balances<'a>(
        &'a self,
        owner: &Address,
        start: Option<AssetId>,
        base_asset_id: &'a AssetId,
        direction: IterDirection,
    ) -> BoxedIter<'a, StorageResult<(AssetId, TotalBalanceAmount)>> {
        match (direction, start) {
            (IterDirection::Forward, None) => {
                self.base_asset_first(owner, base_asset_id, direction)

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Inspect the owner's coins and MessageBalances records for max-value/corrupted entries and repair the state.
  2. When building test chains, keep minted amounts well below u64::MAX.
  3. If this fires on a healthy chain, treat it as storage corruption and report upstream with the owner's state.
Defensive patterns

Strategy: try-catch

Try / catch

// At the GraphQL/API boundary, surface the guard as a clean query error:
match balance_result {
    Ok(total) => Some(total),
    Err(e) if e.to_string().contains("Total balance overflow") => {
        tracing::error!("corrupt balance state for owner {owner:?}");
        None // or a domain-specific GraphQL error
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Querying balance(owner, base_asset_id) when stored coin total + non_retryable message balance > u64::MAX — corrupted balances table, hand-crafted genesis with near-max amounts, or test fixtures summing saturated values.

Common situations: Chains created from custom snapshots with miscomputed supply; storage corruption after hardware faults; integration tests that max out balances.

Related errors


AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16). Data as JSON: /api/errors/98c5a395c6bd1d5d. Report an issue: GitHub.