nautechsystems/nautilus_trader · error

Underflow occurred when subtracting `Money`

Error message

Underflow occurred when subtracting `Money`

What it means

The Sub impl for Money subtracts the raw fixed-point values with checked_sub and panics with this message on underflow (result below MoneyRaw minimum). Both operands must share currency precision. Overflow/underflow of the raw representation is treated as an unrecoverable invariant violation.

Source

Thrown at crates/model/src/types/money.rs:621

}

impl Sub for Money {
    type Output = Self;
    fn sub(self, rhs: Self) -> Self::Output {
        assert_eq!(
            self.currency, rhs.currency,
            "Currency mismatch: cannot subtract {} from {}",
            rhs.currency.code, self.currency.code
        );
        assert!(
            raw_scales_match(self.currency.precision, rhs.currency.precision),
            "Cannot subtract `Money` values with mismatched decimal scales"
        );
        Self {
            raw: self
                .raw
                .checked_sub(rhs.raw)
                .expect("Underflow occurred when subtracting `Money`"),
            currency: self.currency,
        }
    }
}

impl Add<Decimal> for Money {
    type Output = Decimal;
    fn add(self, rhs: Decimal) -> Self::Output {
        self.as_decimal() + rhs
    }
}

impl Sub<Decimal> for Money {
    type Output = Decimal;
    fn sub(self, rhs: Decimal) -> Self::Output {
        self.as_decimal() - rhs
    }
}

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Track the running value in a wider type (i128/Decimal) and clamp or validate before each subtraction.
  2. Check against MONEY range bounds before subtracting.
  3. Compile with high-precision so MoneyRaw is i128.

Example fix

// before
let net = balance - withdrawal; // panics on underflow
// after
let net_dec = balance.as_decimal() - withdrawal.as_decimal();
let net = Money::new(net_dec, USD); // range validated in constructor
Defensive patterns

Strategy: try-catch

Validate before calling

if a.raw.checked_sub(b.raw).is_none() {
    return Err(underflow_err());
}
let diff = a - b;

Type guard

fn sub_will_underflow(a: Money, b: Money) -> bool {
    a.raw.checked_sub(b.raw).is_none()
}

Try / catch

// Money subtraction panics; pre-check before using the operator
if a.raw.checked_sub(b.raw).is_none() {
    // handle underflow (floor at min, alert, escalate)
} else {
    let diff = a - b;
}

Prevention

When it happens

Trigger: money_a - money_b where the raw result is less than MONEY_RAW_MIN, e.g. subtracting a large positive balance from a near-minimum (deeply negative) amount.

Common situations: Computing net exposure or PnL where signed totals swing beyond the raw range;ledger reconciliation code subtracting accumulated balances; excessive negative accumulations from repeated fee deductions.

Related errors


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