System.MathException: Divide by 0
An Integer division by zero. Different from Decimal — `Decimal / 0` returns Infinity in some contexts. Always guard the denominator before dividing, especially with computed denominators from queries.
Also seen asMathException: Divide by 0·Divide by zero·MathException Apex·Arithmetic exception
A monthly report fails at 3 AM. The log shows System.MathException: Divide by 0. The report worked yesterday. The only thing that changed: one of last month's accounts has zero opportunities, so the calculated win-rate denominator is zero. The Apex code does wins / totalOpportunities without checking, and the platform throws on the integer division.
Integer versus Decimal: a critical distinction
MathException: Divide by 0 is specific to integer division. Apex throws it the moment the runtime attempts someInteger / 0. There's no NaN, no Infinity, no quiet zero. The transaction stops.
Decimal division behaves differently. someDecimal / 0 returns Infinity (a special Decimal value) without throwing, depending on the Apex API version. In some versions and contexts, Decimal division by zero also throws. The behavior has shifted across releases; don't rely on either outcome.
The practical takeaway: always guard the denominator before dividing, regardless of numeric type. The guard costs three lines and immunizes you against both the integer exception and the decimal-NaN trap.
The broken example
The win-rate calculation that crashed the monthly report:
public class OpportunityMetrics {
public static Decimal winRate(Id accountId) {
Integer wins = [
SELECT COUNT()
FROM Opportunity
WHERE AccountId = :accountId AND IsClosed = TRUE AND IsWon = TRUE
];
Integer total = [
SELECT COUNT()
FROM Opportunity
WHERE AccountId = :accountId AND IsClosed = TRUE
];
// Throws MathException when total = 0 (no closed opportunities).
return ((Decimal) wins / total) * 100;
}
}
In sandbox data, every test account has closed opportunities, so total > 0 and the report passes. In production, an account with no closed opportunities surfaces, total = 0, and the transaction throws.
The fix: guard the denominator
The minimum viable guard:
public static Decimal winRate(Id accountId) {
Integer wins = [SELECT COUNT() FROM Opportunity WHERE AccountId = :accountId AND IsClosed = TRUE AND IsWon = TRUE];
Integer total = [SELECT COUNT() FROM Opportunity WHERE AccountId = :accountId AND IsClosed = TRUE];
if (total == 0) {
return null; // Or 0, or throw a typed exception. See below.
}
return ((Decimal) wins / total) * 100;
}
The judgment call is what "no data" should return. Three reasonable options:
- Return
null: tells the caller "we don't know." The display layer can render "N/A." - Return
0: tells the caller "zero." The display reads as "0% win rate," which is misleading if the account has never had an opportunity. - Throw a typed exception: forces the caller to handle the missing-data case explicitly.
For a metric like win-rate, null is usually the cleanest semantic. "No data" is a different concept from "zero," and conflating them leads to misleading dashboards.
The fixed example, full version
A production-shaped win-rate calculator with all the guards in place:
public class OpportunityMetrics {
public class WinRateResult {
@AuraEnabled public Decimal percent;
@AuraEnabled public Integer totalClosed;
@AuraEnabled public Integer wins;
@AuraEnabled public Boolean hasData;
}
public static WinRateResult winRate(Id accountId) {
if (accountId == null) {
throw new IllegalArgumentException('accountId is required');
}
AggregateResult[] results = [
SELECT
COUNT(Id) totalClosed,
SUM(CASE WHEN IsWon = TRUE THEN 1 ELSE 0 END) wins
FROM Opportunity
WHERE AccountId = :accountId AND IsClosed = TRUE
];
WinRateResult r = new WinRateResult();
r.totalClosed = (Integer) results[0].get('totalClosed');
r.wins = (Integer) (results[0].get('wins') ?? 0);
r.hasData = r.totalClosed > 0;
r.percent = r.hasData ? (((Decimal) r.wins / r.totalClosed) * 100).setScale(2) : null;
return r;
}
}
A few production-grade improvements rolled in: input validation, a single aggregate query (instead of two), a typed return shape that includes "no data" as a first-class state, and explicit scale on the resulting decimal.
Where divide-by-zero hides
Beyond the obvious a / b pattern, the same fault can appear in:
Modulo operations. someInteger / 0 throws; so does someInteger // 0 and Math.mod(x, 0). Modulo by zero has the same semantic problem as division by zero.
Computed denominators from queries. Counting records and dividing is the most common production source. count(*) against any filtered set can return zero if the filter excludes everything.
Currency conversion. amount / exchangeRate where exchangeRate defaults to zero in a misconfigured row.
Date math via day count. daysBetween / weeksInPeriod where weeks is computed from another formula that might be zero.
Pricing engines. discount / msrp where MSRP is zero for free items.
Each of these benefits from the same defensive pattern: check the denominator before dividing.
When the platform doesn't throw but lies
Decimal division by zero in some Apex versions returns Infinity instead of throwing. The next operation on the Infinity propagates: Infinity * 0 is NaN, NaN + anything is NaN, and your final result is a Decimal that prints as "NaN" but compares as not-equal to itself.
If a metric in production reads "NaN" or shows up as null when it shouldn't, walk back through the calculation looking for a zero denominator that wasn't guarded. The platform didn't throw; the operation silently produced garbage. The diagnostic surface is in the data, not the log.
A defensive habit: always run Decimal.isFinite() (where available) or check result < 0 || result > 0 to catch NaN and Infinity before letting them propagate.
Triggers and validation as a defense
For business-critical metrics, consider adding a validation rule on the source data:
Rule: Closed_Won_Count_Required
Condition: AND(IsClosed = TRUE, IsWon = TRUE, ISNULL(Amount))
Error: 'Closed-won opportunities must have an Amount.'
Validation rules don't directly prevent divide-by-zero, but they enforce data shape that makes the math reliable. An opportunity without an Amount is a common cause of divide-by-zero downstream when revenue-per-opportunity calculations run.
Formula fields and divide-by-zero
Formula fields have their own divide-by-zero behavior. The formula language returns #Error! for any division by zero in a formula field. The field displays the error string on the record, and any downstream calculation that uses the formula's value also produces #Error!.
The fix in formulas is the same as in Apex: wrap with IF(denominator = 0, null, numerator / denominator). The formula language supports null results, which display as empty rather than as #Error!.
Some formulas use IFERROR() to translate divide-by-zero into a sensible fallback:
IFERROR(Wins__c / Total_Opps__c * 100, 0)
The pattern is concise and prevents the formula from poisoning every report that references it.
Testing the divide-by-zero path
A test that explicitly seeds the zero-denominator case:
@isTest
static void winRate_returnsNullWhenNoData() {
Account a = new Account(Name = 'Test Account No Opps');
insert a;
Test.startTest();
OpportunityMetrics.WinRateResult r = OpportunityMetrics.winRate(a.Id);
Test.stopTest();
System.assertEquals(false, r.hasData, 'No opps means no data');
System.assertEquals(null, r.percent, 'Win-rate percent should be null, not zero');
System.assertEquals(0, r.totalClosed);
}
Pair it with a test for the happy path (some opportunities) and a test for a mixed case (all losses). Three tests cover the full state space.
A pattern that scales beyond a single method
When divide-by-zero appears repeatedly across a codebase, it's worth introducing a small helper:
public class SafeMath {
public static Decimal divideOrNull(Decimal numerator, Decimal denominator) {
if (denominator == null || denominator == 0) return null;
return numerator / denominator;
}
public static Decimal divideOrZero(Decimal numerator, Decimal denominator) {
if (denominator == null || denominator == 0) return 0;
return numerator / denominator;
}
public static Decimal percentage(Decimal part, Decimal whole, Integer scale) {
Decimal result = divideOrNull(part, whole);
return result == null ? null : (result * 100).setScale(scale);
}
}
A single class owns the safe-division logic. Every caller spells out which semantic they want. The pattern is verbose but eliminates the bug class from your codebase.
Naming matters here. divide would look generic and tempt callers to skip the safety check. divideOrNull and divideOrZero make the safety property visible at the call site. Anyone reading the code knows exactly what happens when the denominator is zero.
Why the platform throws instead of returning a sentinel
Several languages return special sentinel values for divide-by-zero: NaN in JavaScript, Infinity in many floating-point implementations, None in nullable systems. Apex's integer division throws because the alternatives have downsides.
Returning 0 is misleading: zero is a real result that has different meaning from "undefined." Returning a sentinel like MAX_VALUE requires every caller to remember to check, and forgetting silently propagates garbage. Returning null from a primitive division would force every numeric expression to handle null, complicating every formula.
Throwing puts the diagnosis at the failure site and forces the developer to make an explicit choice about what "no data" means. The trade-off is that ungaurded production code crashes, which is louder than producing garbage. Most teams prefer the loud crash.
Watch for the multi-step calculation
A trap that hits otherwise careful code: a calculation passes through multiple steps, and the first step produces a zero that isn't checked before the second step divides by it.
Decimal averageDealSize = totalRevenue / numDeals; // Could be zero if numDeals is large
Decimal commissionRate = bonusPool / averageDealSize; // Throws if averageDealSize is 0
The fault is at the second division, but the cause is the first. The diagnostic is to trace the denominator's history back to its source and check at every step.
Some teams adopt a rule: "every division produces an Optional<Decimal>-shaped result that the next step has to unwrap." It's verbose but catches multi-step zero propagation at the type level.
A subtle interaction with rounding
Apex Decimal.divide(divisor, scale) lets you specify the result precision. The variant Decimal.divide(divisor, scale, roundingMode) adds a rounding strategy. Neither variant skips the zero check; both throw if divisor is zero.
If you want explicit control over rounding and divide-by-zero handling, wrap the call:
public static Decimal safeDivide(Decimal n, Decimal d, Integer scale) {
if (d == null || d == 0) return null;
return n.divide(d, scale, System.RoundingMode.HALF_EVEN);
}
The two concerns are independent: precision is about the result's shape, divide-by-zero is about whether the operation makes sense. Handle them separately.
How dashboards and reports propagate the error
A Salesforce report that uses a formula field with divide-by-zero in its definition shows #Error! in the cell. The cell value doesn't aggregate cleanly; SUM over #Error! values returns #Error!. A dashboard tile downstream of the report displays an error indicator.
The chain of effects is sometimes opaque: a dashboard says "report failed," the report says one column has #Error!, the column comes from a formula, the formula does an unchecked division. Three steps to the root cause.
The fix is the same as in Apex: wrap the division in the formula with IF(denominator = 0, null, ...) or IFERROR(... , null). Once the formula returns null instead of #Error!, the report and dashboard handle the missing data gracefully.
Defensive habits beyond the literal fix
Three habits prevent most appearances of this error in a mature codebase.
Never trust a count, sum, or aggregate to be non-zero. Any value derived from a query or a filter could be zero on some path through the data. Treating zero as the always-special case at the bottom of the computation is cheaper than trying to prove it can't happen at the top.
Type-check before dividing. If the denominator comes from r.get('field') on an AggregateResult, it could be null (not just zero). Cast and check both conditions: if (val == null || val == 0).
Run reports against an empty sandbox. Most divide-by-zero bugs hide in code paths that the populated sandbox never exercises. A "smoke test sandbox" with deliberately sparse data catches edge cases that the busy sandbox never sees.
These three habits add maybe ten minutes per non-trivial calculation. They eliminate the bug class. The cost-benefit is overwhelmingly in favor of the discipline.
A final word on transactional semantics
When MathException fires inside a trigger, the whole DML rolls back. The user sees a generic save error and the data is unchanged. This is usually the right behavior: a calculation that can't complete shouldn't half-write derived fields.
When it fires inside a Queueable or Batch Apex execute(), the chunk fails but other chunks continue. The Apex Job log records the failure; recovery requires re-running the failed chunk. Wrap critical async calculations in try/catch so the chunk continues even if one record's math fails, and log the failure for post-hoc review.
When it fires inside a synchronous web service called from Lightning, the user gets an AuraHandledException and sees a generic error in the UI. Translate the exception with a meaningful message: "Could not calculate win rate because the account has no closed opportunities."
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Apex errors
Initial term of field expression must be a concrete SObject: <type>
ApexYou wrote `someThing.Field__c` where `someThing` isn't a specific SObject type — usually an `SObject` or `Object` reference. Apex needs the …
Method does not exist or incorrect signature
ApexThe Apex compiler can't find a method matching the call you wrote — wrong name, wrong argument types, or wrong number of arguments. The comp…
MIXED_DML_OPERATION: DML operation on setup object is not allowed after you have updated a non-setup object (or vice versa)
ApexSalesforce splits objects into "setup" (User, Group, GroupMember, PermissionSet, Profile, Queue, etc.) and "non-setup" (everything else). A …
System.AsyncException: Future method cannot be called from a future or batch method
ApexYou called an `@future` method from inside another `@future`, batch, or queueable. Salesforce blocks recursive async chains because they cou…
System.CalloutException: Read timed out
ApexThe HTTP callout exceeded its allowed read time waiting for the remote server's response. Salesforce caps a single callout at 120 seconds (d…