· via dev.to (home feed)
rust_decimal's default parse returns Ok while dropping the last wei of token amounts
A dev.to comparison of Rust money types shows rust_decimal's from_str accepting a 30-digit token amount while silently discarding its final digit; from_str_exact returns Err(Underflow) on the same input.

A parse that succeeds with fewer digits than you sent
According to a comparison of Rust money types published on dev.to by Tom Wang, a fintech and crypto-payments developer, rust_decimal's default string constructor reports success on input it cannot fully store.
The demonstration: calling Decimal::from_str on "100000000000.000000000000000001" returns Ok, but the value held afterwards is 100000000000.00000000000000000. The input carries twelve integer digits and eighteen fractional digits — thirty significant digits — while rust_decimal's 96-bit mantissa holds twenty-eight. The trailing 1, which in an 18-decimal token amount is the last wei, is discarded to make the value fit, with no error raised. Running the same string through Decimal::from_str_exact produces Err(Underflow) instead.
Wang says he verified the behaviour in a scratch crate against rust_decimal 1.43.0, bigdecimal 0.4.11 and Rust 1.98, with every output pasted from that run. The dev.to post summarises a longer write-up on the author's own site, so the comparison is one developer's benchmark rather than an upstream bug report.
Why the type behaves this way
rust_decimal fits a decimal number into 16 fixed bytes: a 96-bit integer, a scale between 0 and 28, and a sign bit. That buys exact values up to ±79,228,162,514,264,337,593,543,950,335, Copy semantics and no heap allocation, with 28 significant digits available in most cases — 29 in some, per the article. The limit is hard: anything past the twenty-eighth significant digit must be rounded away or rejected, and the default parse path rounds. The crate's default rounding mode is half-even. A strict variant exists, since from_str_exact turns the same condition into an error, but nothing about the default path's Ok result hints that digits were lost.
The fiat/token gap that produces such strings
The article's wider argument is that the usual search-result summary — "fixed-size and fast versus arbitrary precision" — is true but not enough to choose a type with. Its practical breakdown:
- f64 fails the first test anyone runs: adding 0.1 ten times gives 0.9999999999999999, and 0.1 + 0.2 == 0.3 evaluates to false.
- i64 minor units are exact and are what Stripe and Adyen put on the wire; one i64 holds 92,233,720,368,547,758.07 in pence, or 9.223372036854775807 ETH when counted in wei.
- i128/u128 minor units cover token balances and TigerBeetle-style ledgers, up to roughly 3.4 × 10³⁸.
- rust_decimal suits rates, fees, tax, FX and API amounts.
- bigdecimal — a heap-backed BigInt with an i64 scale and unbounded precision — belongs at boundaries where inputs can exceed 28 digits.
The truncation example sits exactly at that boundary: an amount with twelve integer digits leaves only sixteen fractional digits of room inside a 28-digit budget, while 18-decimal token amounts demand eighteen.
The rest of the crate picture
The post also tabs fastnum 0.7.5: fixed precision tiers of 19, 38, 77 and 154 digits, half-up as the default rounding mode, support for NaN and Infinity, and serde settings that emit strings but only accept strings. rust_decimal and bigdecimal both serialise to strings and accept either strings or numbers on input. For Postgres, rust_decimal supports sqlx, Diesel and tokio-postgres; bigdecimal covers sqlx and Diesel; fastnum covers all three. As of 3 November 2026 the article lists total downloads of 169.1M for rust_decimal, 139.5M for bigdecimal and 10.6M for fastnum, with latest releases on 2 September 2026, 28 September 2026 and 11 June 2026 respectively, fastnum still pre-1.0.
Why it matters
Silent precision loss in a type whose entire purpose is exact money arithmetic is close to the worst failure mode available, because it is invisible at the point of parsing. A ledger that records a deposit using from_str stores one wei less than the sender moved; nothing errors, and the discrepancy only surfaces later as a reconciliation break. The mitigation is cheap and local: parse untrusted or chain-derived amounts with from_str_exact, keep balances in integer minor units, and use bigdecimal at the edges where inputs can outrun 28 digits. The deeper point the article makes is that "exact decimal" is a range claim, not a guarantee — 28 digits comfortably covers fiat amounts and still loses the tail of an 18-decimal token with a twelve-digit integer part.
- #rust
- #rust-decimal
- #bigdecimal
- #money
- #precision
- #fintech