🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
java.lang.ArithmeticException: Rounding necessary
at java.base/java.math.BigDecimal.commonNeedIncrement(BigDecimal.java:4882)
at java.base/java.math.BigDecimal.needIncrement(BigDecimal.java:4932)
at java.base/java.math.BigDecimal.divideAndRound(BigDecimal.java:4846)
at java.base/java.math.BigDecimal.setScale(BigDecimal.java:2900)
at java.base/java.math.BigDecimal.setScale(BigDecimal.java:2960)
at com.devinhyderabad.invoice.MoneyFormatter.cents(MoneyFormatter.java:30)⚡ Quick Fix Works 80% of the time
Pass an explicit rounding mode to setScale.
BigDecimal price = raw.setScale(2, RoundingMode.HALF_UP);🧠 Why this Happens
Tap to expand the deep technical explanation
One-arg setScale(int) delegates to the two-arg overload with RoundingMode.UNNECESSARY - a CONTRACT, not a default: the caller asserts no information will be lost. Reducing scale requires dividing by a power of ten, which is why the refusal genuinely travels through divideAndRound despite the misleading name (verified against Temurin 25: the captured frames ARE the setScale path). commonNeedIncrement fires "Rounding necessary" when discarded digits are non-zero, because honoring both the new scale and exactness is impossible. Also verified: intValueExact()/longValueExact() route through this very setScale chain, so the same exception backs the *Exact family - their extra trick is throwing a distinct overflow message when the magnitude does not fit.
The HITEC City Parking Spot Analogy:
Signing a form that promises "no cents were dropped" while your amount still holds a half-cent: the notary stops you at the desk.
🔁 How to Reproduce Confirm this is your error
new BigDecimal("1.234").setScale(2); // ArithmeticException: Rounding necessary
🛠️ Solutions (5 Ways to Fix)
Provide the intended RoundingMode explicitly
👉 Use this whenever reducing scale for display or storage.
Naming HALF_UP (or your policy) replaces the accidental UNNECESSARY assertion with deliberate, documented behavior.
BigDecimal net = gross.setScale(2, RoundingMode.HALF_UP);Use HALF_EVEN for financial aggregates
👉 Use this if many rounded values get summed later.
Banker rounding eliminates upward bias across large datasets; it is the accounting-standard choice for ledgers and tax splits.
total = line.setScale(2, RoundingMode.HALF_EVEN).add(total);Reserve *Exact() methods for genuinely exact flows
👉 Use this if converting stored decimals to primitives.
intValueExact()/longValueExact() are safety tools that PROVE no fraction/overflow exists; if they can throw in normal operation, use plain intValue plus range checks instead.
int units = quantity.intValueExact(); // only after validating whole numbersSeparate storage precision from display formatting
👉 Use this if UI needs fewer decimals than the database stores.
Keep full precision on entities; format at presentation via NumberFormat/String so setScale calls (and their exceptions) vanish from domain logic.
NumberFormat money = NumberFormat.getCurrencyInstance();
label.setText(money.format(fullPrecisionValue));Flag mode-less BigDecimal calls with IDE inspection gates
👉 Use this if the team wants every divide()/setScale() without an explicit RoundingMode caught before merge.
IntelliJ's BigDecimalMethodWithoutRoundingCalled inspection is type-aware, so it catches setScale(scale), setScale(10) and setScale(SCALE) alike - exactly the calls a text grep misses - and runs headless in CI via inspect.sh or Qodana. SpotBugs users get equivalent coverage from the inspequte plugin rule BIGDECIMAL_SET_SCALE_WITHOUT_ROUNDING.
# CI gate: IntelliJ inspections, headless (Qodana-compatible)
$IDEA_HOME/bin/inspect.sh <project> <profile> <outDir> -v2
# Inspection ID: BigDecimalMethodWithoutRoundingCalled
# reports divide()/setScale() lacking a rounding-mode argument.
# Provably-exact call sites opt out explicitly:
# //noinspection BigDecimalMethodWithoutRoundingCalled📋 Version Notes
"Rounding necessary" wording stable since RoundingMode introduction.
Unchanged. stripTrailingZeros() fixed for whole numbers like 100.00 around Java 8 - older JDKs left trailing zeros.
🛡️ How to Prevent This Next Time
Every scale change states its rounding mode. Treat UNNECESSARY as an assertion tool for provably-exact paths only.