I still see arithmetic bugs ship in codebases that otherwise have great tests and solid TypeScript. The reason is simple: JavaScript arithmetic looks like every other language until it doesn’t—usually when values arrive as strings, when floating-point rounding shows up in money-like calculations, or when an innocent ++ inside a larger expression changes evaluation order.
If you write frontend forms, Node services, data pipelines, or anything that touches user input, you’re doing arithmetic all the time: computing totals, durations, pagination offsets, percentages, and rate limits. When those results are wrong, the UI looks “off by one,” invoices don’t match, or analytics drift.
I’m going to walk you through the arithmetic operators you use daily (+, -, , /, %, *, ++, --, unary + and unary -), but with the real-world behavior I rely on in production: coercion rules, precedence, NaN/Infinity/-0, and how BigInt changes the picture. You’ll get runnable snippets, common failure patterns, and the guardrails I recommend in 2026 (lint rules, type checks, and a couple of tiny helper patterns).
The Two Number Worlds: Number vs BigInt
JavaScript has two numeric primitives you’ll meet in arithmetic:
Number: IEEE-754 double precision floating point. Fast, ubiquitous, and capable of decimals,Infinity, andNaN.BigInt: arbitrary-precision integers (no decimals). Great for IDs, counters, and exact integer math that might exceedNumber.MAXSAFEINTEGER.
I treat this as a “two-currency” system. Number is like cash you can make change with, but sometimes you get rounding pennies. BigInt is like a ledger: exact integers, but no fractions.
Key behaviors that influence every operator:
Numberis floating-point:0.1 + 0.2is not exactly0.3.BigIntandNumberdo not mix in arithmetic:1n + 1throws aTypeError.- Division differs:
5 / 2is2.5(Number), but5n / 2nis2n(BigInt truncates toward zero).
Runnable check:
// Run with: node arithmetic-basics.js
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(5 / 2); // 2.5
console.log(5n / 2n); // 2n
I recommend picking one numeric world per “domain”:
- Money-like or measurements: often
Number, but stored as integers of smallest unit (cents, milliseconds) to avoid floating-point drift. - IDs, counters, exact integer math, large ranges:
BigInt.
A subtle but important extension of that rule: I try not to bounce between the worlds inside the same “flow.” Converting BigInt to Number is lossy once you’re above Number.MAXSAFEINTEGER, and converting Number to BigInt requires an explicit decision about rounding.
const tooBig = 9007199254740993; // Number cannot represent this exactly
console.log(tooBig); // 9007199254740992 (already rounded)
const okBig = 9007199254740993n;
console.log(okBig); // 9007199254740993n
When I’m designing APIs, I often pick:
- Internal computation:
BigIntfor counters/IDs when exactness matters. - Transport: strings for those IDs.
- UI: keep them as strings; only parse when you truly must do arithmetic.
Addition (+) and Unary Plus (+value): Sum vs String Glue
The + operator is the one that causes the most surprises because it’s both:
- numeric addition, and
- string concatenation.
The rule I keep in my head: if either operand becomes a string during evaluation, + becomes “string glue.” That includes cases where you didn’t realize you were holding a string.
// Numeric addition
const apples = 2;
const oranges = 3;
console.log(apples + oranges); // 5
// String concatenation
const uiCount = ‘2‘;
console.log(uiCount + 3); // ‘23‘
If you’re reading from form fields, query params, JSON, or environment variables, you’re often holding strings. This is where unary plus shines.
Unary plus (+value) attempts to convert to a Number.
const widthText = ‘1280‘;
const heightText = ‘720‘;
const width = +widthText;
const height = +heightText;
console.log(width + height); // 2000
But unary plus has sharp edges: invalid numeric strings turn into NaN.
const raw = ‘12px‘;
const parsed = +raw;
console.log(parsed); // NaN
console.log(parsed + 1); // NaN
For user-facing parsing, I prefer explicit parsing with validation:
const rawPixels = ‘12px‘;
const value = Number.parseInt(rawPixels, 10);
console.log(value); // 12
Practical guidance I use:
- If the value must be a full number string, use
Number(raw)and validate withNumber.isFinite. - If you expect units, use
parseInt/parseFloat, then validate. - If concatenation is intended, make it obvious with template strings.
const first = ‘Ada‘;
const last = ‘Lovelace‘;
console.log(${first} ${last});
A coercion trap I see constantly
const subtotal = ‘19.99‘;
const tax = 1.65;
console.log(subtotal + tax); // ‘19.991.65‘ (string glue)
Fix it with conversion at the boundary:
const subtotal = Number(‘19.99‘);
const tax = 1.65;
console.log(subtotal + tax); // 21.64
Boundary parsing pattern (the one I actually use)
If I had to boil down “safe arithmetic” to one habit, it’s this: parse once, validate once, then treat values as already-clean everywhere else.
function toFiniteNumber(raw) {
const n = typeof raw === ‘number‘ ? raw : Number(raw);
return Number.isFinite(n) ? n : null;
}
const quantity = toFiniteNumber(‘2‘);
const unitPrice = toFiniteNumber(‘19.99‘);
if (quantity === null || unitPrice === null) {
throw new Error(‘Invalid numeric input‘);
}
console.log(quantity * unitPrice); // 39.98
The reason I like returning null is practical: it forces me to handle the “bad input” branch. If I return NaN, it tends to sneak into later arithmetic and poison everything.
Subtraction (-) and Unary Negation (-value): Numbers Only, So Coercion Looks Different
Subtraction is simpler than addition: it’s always numeric. If the operands aren’t numbers, JavaScript tries to convert them to numbers, and you typically get NaN when conversion fails.
console.log(10 – 7); // 3
console.log(‘10‘ – 7); // 3 (string converted to number)
console.log(‘Hello‘ – 1); // NaN
Unary negation flips the sign after converting to a number:
console.log(-5); // -5
console.log(-‘6‘); // -6
console.log(-‘6.25‘); // -6.25
console.log(-‘6px‘); // NaN
I reach for unary negation a lot when normalizing “directional” values:
// A UI slider might send ‘1‘ or ‘-1‘
const direction = Number(‘1‘);
const step = 20;
console.log(direction * step); // 20
console.log((-direction) * step); // -20
If you need to preserve the difference between “missing” and “invalid,” don’t rely on coercion. Treat parsing as a separate step:
function toFiniteNumber(raw) {
const n = typeof raw === ‘number‘ ? raw : Number(raw);
return Number.isFinite(n) ? n : null;
}
console.log(toFiniteNumber(‘12‘)); // 12
console.log(toFiniteNumber(‘12px‘)); // null
A common bug: subtracting timestamps that aren’t timestamps
In UI and service code, I often see time arithmetic done on values that might be Date, ISO strings, or milliseconds, depending on the call site.
My rule: represent durations as integer milliseconds (or seconds) as early as possible.
const startMs = Date.parse(‘2026-02-01T12:00:00Z‘);
const endMs = Date.parse(‘2026-02-01T12:00:10Z‘);
const durationMs = endMs – startMs;
console.log(durationMs); // 10000
And I validate the parse:
const maybeMs = Date.parse(‘not-a-date‘);
console.log(Number.isFinite(maybeMs)); // false (it’s NaN)
Multiplication (*) and Division (/): Infinity, -0, and NaN Are Real Values
Multiplication and division are numeric-only operators, so they coerce operands to numbers.
console.log(3 * 3); // 9
console.log(-4 * 4); // -16
console.log(‘6‘ * ‘7‘); // 42
console.log(‘hi‘ * 2); // NaN
Division has a few behaviors that matter in production:
- Division by zero does not throw for
Number. You getInfinity,-Infinity, orNaN. -0exists and can appear from division.
console.log(5 / 2); // 2.5
console.log(3 / 0); // Infinity
console.log(-3 / 0); // -Infinity
console.log(0 / 0); // NaN
console.log(2 / -0); // -Infinity
If you’ve never had -0 cause trouble: it often shows up in formatting, comparisons, and boundary logic.
const value = -0;
console.log(value === 0); // true
console.log(Object.is(value, -0)); // true
console.log(Object.is(value, 0)); // false
In UI code, I typically normalize -0 to 0 right before display:
function normalizeNegativeZero(n) {
return Object.is(n, -0) ? 0 : n;
}
console.log(normalizeNegativeZero(-0)); // 0
Integer division patterns
JavaScript doesn’t have a distinct integer division operator for Number. I usually do:
Math.trunc(a / b)for “drop the fractional part,”Math.floor(a / b)for “round down,” especially for pagination, and- validate
b !== 0whenInfinitywould be a bug.
const totalItems = 53;
const pageSize = 10;
const pages = Math.ceil(totalItems / pageSize);
console.log(pages); // 6
Guarding division in real systems
In production, “division by zero” is rarely literally 0. It’s more often:
- an empty dataset count,
- a missing configuration value,
- a time delta that is
0because two timestamps are equal, - a bad parse that became
0via coercion.
So I don’t just check b === 0; I check “finite and non-zero.”
function safeDivide(a, b) {
if (!Number.isFinite(a) |
b === 0) return null;
const result = a / b;
return Number.isFinite(result) ? result : null;
}
console.log(safeDivide(10, 2)); // 5
console.log(safeDivide(10, 0)); // null
When I do want a defined behavior, I make it explicit.
function ratioOrZero(a, b) {
if (!Number.isFinite(a) |
b === 0) return 0;
return a / b;
}
Remainder (%): Not Modulo, and the Sign Rule Matters
The % operator returns the remainder after division. In JavaScript (like many languages), it’s a remainder operator, not a mathematical modulo.
The rule that surprises people: the result takes the sign of the dividend (the left operand).
console.log(9 % 5); // 4
console.log(-12 % 5); // -2
console.log(1 % -2); // 1
console.log(5.5 % 2); // 1.5
console.log(NaN % 2); // NaN
When I need a true modulo that’s always non-negative (common in circular buffers and calendar math), I wrap it:
function mod(n, m) {
return ((n % m) + m) % m;
}
console.log(mod(-1, 5)); // 4
console.log(mod(6, 5)); // 1
Real-world example: cycling through theme presets.
const presets = [‘light‘, ‘dark‘, ‘sepia‘, ‘high-contrast‘];
let index = 0;
function nextPreset(direction) {
index = mod(index + direction, presets.length);
return presets[index];
}
console.log(nextPreset(1)); // ‘dark‘
console.log(nextPreset(1)); // ‘sepia‘
console.log(nextPreset(-1)); // ‘dark‘
% for “every Nth event” (and the off-by-one I watch for)
If you’re doing sampling, batching, or rate-limited logs, % is great.
let count = 0;
function shouldLogEvery(n) {
count += 1;
return count % n === 0;
}
console.log(shouldLogEvery(3)); // false (1)
console.log(shouldLogEvery(3)); // false (2)
console.log(shouldLogEvery(3)); // true (3)
But note the decision: is the first item “1” or “0”? If you want the first item to log too, you’d start from 0 and check before incrementing.
Exponentiation (): Right-Associative and Precedence Gotchas
Exponentiation raises the left operand to the power of the right operand.
console.log(2 5); // 32
console.log(3 3); // 27
console.log(10 -2); // 0.01
Two details I care about:
1) It’s right-associative.
console.log(2 3 2); // 512 because 2 (3 2) => 2 9
2) You can’t put a unary operator directly on the base without parentheses.
// This is a syntax error:
// const bad = -4 2;
const ok = -(4 2);
console.log(ok); // -16
For performance and stability, I try not to use for repeated multiplication in hot loops if the exponent is small and known. That’s not because is “slow” in general—engines are very good at it—but because it’s easy to accidentally compute powers on arrays of coerced values and spend 5–20ms on a UI thread converting and validating. The bigger win is usually to validate inputs once, then compute.
Practical uses I see in production
- Scaling values: converting between units (
bytes -> KB/MBis usually division, but sometimes you’re doing2 10factors). - Scoring curves: easing functions and decay (
score (decay * t)). - UI geometry: area scaling (
r 2).
When I use it for decay, I clamp inputs because it’s easy to generate subnormals, Infinity, or just values too small to matter.
function decay(value, factor, steps) {
if (!Number.isFinite(value) |
!Number.isFinite(steps)) return null;
if (factor 1) return null;
const out = value (factor * steps);
return Number.isFinite(out) ? out : null;
}
Increment (++) and Decrement (--): I Avoid Them Outside Tight Loops
++ and -- change a variable by 1 and return a value. They come in two forms:
- postfix:
x++returns the old value, then increments - prefix:
++xincrements, then returns the new value
let a = 2;
const b = a++; // b = 2, a = 3
let x = 5;
const y = ++x; // x = 6, y = 6
console.log({ a, b, x, y });
I’m fine with these in simple loops:
let sum = 0;
for (let i = 0; i < 5; i++) {
sum += i;
}
console.log(sum); // 10
But I actively discourage mixing ++/-- with other operations in a single expression. It makes code harder to review and can lead to subtle order-of-evaluation bugs.
Instead of:
let index = 0;
const value = items[index++] + items[index++];
I prefer:
let index = 0;
const first = items[index];
index += 1;
const second = items[index];
index += 1;
const value = first + second;
That’s longer, but the logic is obvious and less fragile when refactoring.
Why this matters even more with async and callbacks
A pattern that bites teams is mutating counters in places that can be re-entered.
- UI event handlers can fire quickly.
- Streams can call back in surprising groupings.
- Retry logic can run the same function twice.
If I need an increment in those contexts, I make it a single obvious statement and (if it’s shared state) I guard it.
Mixing Types Safely: Strings, NaN, null, and BigInt Boundaries
Arithmetic operators force you to confront JavaScript’s type conversions. Here’s how I keep it sane.
Treat parsing as a boundary concern
When you cross a boundary (DOM input, HTTP request, environment variable), parse once and validate.
function parseAmountCents(raw) {
// Accepts ‘12.34‘ and converts to integer cents
const n = Number(raw);
if (!Number.isFinite(n)) return null;
return Math.round(n * 100);
}
const cents = parseAmountCents(‘19.99‘);
if (cents === null) {
console.error(‘Invalid amount‘);
} else {
console.log(cents); // 1999
}
Know what becomes 0
These values become 0 when coerced to Number in arithmetic (and that can hide bugs):
nullbecomes0‘‘(empty string) becomes0‘ ‘(spaces) becomes0
console.log(null + 1); // 1
console.log(‘‘ – 0); // 0
console.log(‘ ‘ * 2); // 0
If null means “missing,” you probably don’t want it treated as 0. I recommend defaulting explicitly:
const maybeCount = null;
const count = maybeCount ?? 0;
console.log(count + 1); // 1
BigInt: exact integers with strict separation
Use BigInt for exact integer arithmetic and ranges beyond safe integers:
const maxSafe = Number.MAXSAFEINTEGER;
console.log(maxSafe + 1 === maxSafe + 2); // true (precision loss)
const exact = 9007199254740991n;
console.log(exact + 1n === exact + 2n); // false
But don’t mix BigInt and Number:
// Throws TypeError:
// console.log(1n + 1);
Convert intentionally at the boundary:
const idFromDb = 12345678901234567890n;
const asString = idFromDb.toString();
console.log(asString);
In services, I often serialize BigInt as strings in JSON (since JSON.stringify doesn’t support BigInt directly) and parse it back.
A coercion “truth table” I keep in mind
I don’t memorize everything, but there are a few conversions that show up constantly:
Number(true)is1,Number(false)is0.Number(undefined)isNaN.Number(null)is0.Number(‘‘)is0.Number(‘ ‘)is0.Number(‘0x10‘)is16(hex parsing) — this surprises people.
If those are not acceptable in your domain, don’t coerce implicitly. Parse with a stricter policy.
function parseStrictDecimal(raw) {
if (typeof raw === ‘number‘) return Number.isFinite(raw) ? raw : null;
if (typeof raw !== ‘string‘) return null;
if (!/^[+-]?(\d+)(\.\d+)?$/.test(raw.trim())) return null;
const n = Number(raw);
return Number.isFinite(n) ? n : null;
}
Practical Patterns, Mistakes I See, and 2026 Guardrails
Here are the failure modes I see most often—and what I do instead.
Mistake: floating-point for money without a strategy
const total = 0.1 + 0.2;
console.log(total); // 0.30000000000000004
My go-to approach: store “smallest unit” integers (cents, milliseconds), and only format for display.
const priceCents = 1999;
const taxCents = 165;
const totalCents = priceCents + taxCents;
console.log(totalCents); // 2164
Mistake: + hiding string concatenation
If you’re building strings, use template strings. If you’re summing, convert early.
const quantityText = ‘2‘;
const unitPriceText = ‘19.99‘;
const quantity = Number(quantityText);
const unitPrice = Number(unitPriceText);
if (!Number.isFinite(quantity) || !Number.isFinite(unitPrice)) {
throw new Error(‘Invalid input‘);
}
console.log(quantity * unitPrice); // 39.98
Mistake: remainder used as modulo with negatives
Use mod() for wraparound math.
Guardrails I recommend in 2026
I rely on tooling to keep arithmetic boring:
- TypeScript with
noUncheckedIndexedAccessand strict settings where possible. - ESLint rules that discourage confusing expressions (especially around
++and implicit coercion). - Unit tests around boundary parsing and rounding.
A quick “Traditional vs Modern” view:
Traditional approach
—
Implicit coercion (value * 1)
Number(value), Number.isFinite) Floating-point totals
index % n
mod(index, n) to handle negatives Number and hope
BigInt + string serialization toFixed everywhere
If you use AI-assisted coding (which I do daily), the trap is that generated code often “works” on happy-path inputs and quietly relies on coercion. I always ask for one extra thing: “Show me what happens with empty strings, null, negative values, and very large numbers.” Then I bake those cases into tests.
Operator Precedence and Evaluation Order: The Bugs Aren’t in the Operator, They’re in the Expression
Most arithmetic mistakes I debug aren’t “someone didn’t know what * does.” They’re precedence and evaluation-order issues inside longer expressions.
A few rules I actually use:
binds tighter than*and/, and it’s right-associative.*and/bind tighter than+and-.- Parentheses are cheap. Production incidents are expensive.
Here’s a classic:
console.log(10 + 2 * 3); // 16, not 36
console.log((10 + 2) * 3); // 36
When expressions get longer, I stop trusting my “mental precedence engine” and rewrite.
Instead of:
const total = subtotal + subtotal taxRate – discount (1 + feeRate);
I’ll do:
const tax = subtotal * taxRate;
const discountWithFee = discount * (1 + feeRate);
const total = subtotal + tax – discountWithFee;
It’s easier to test too: I can unit-test tax and discountWithFee separately.
Short-circuiting is not arithmetic, but it often sits next to it
When arithmetic is mixed with &&, ||, or ??, I get extra cautious because these operators can return non-boolean values.
If I need a default, I prefer ?? over || for numeric values because 0 is valid.
const userProvided = 0;
console.log(userProvided || 10); // 10 (probably wrong)
console.log(userProvided ?? 10); // 0 (correct)
NaN, Infinity, and “Finite”: How I Decide What’s Valid
I treat NaN and Infinity as “valid JavaScript values” but “invalid business values” most of the time.
A few key properties:
NaNis the only value in JS that is not equal to itself.Number.isNaN(x)is the safe check; globalisNaN(x)coerces.Number.isFinite(x)is my go-to validation gate.
const x = NaN;
console.log(x === x); // false
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN(‘NaN‘)); // false
console.log(isNaN(‘NaN‘)); // true (coerces, often unwanted)
console.log(Number.isFinite(12)); // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite(NaN)); // false
A tiny helper I reuse everywhere
If I can only introduce one helper in a codebase, it’s something like this:
function assertFiniteNumber(n, message = ‘Expected a finite number‘) {
if (!Number.isFinite(n)) {
throw new Error(${message}: ${String(n)});
}
return n;
}
I’ll use it right after parsing or right before critical calculations.
Rounding: Decide Where You Round, Then Round Once
Floating-point issues aren’t “a JavaScript problem.” They’re a binary floating-point reality problem. The fix is rarely “hope harder.” The fix is choosing a rounding strategy that matches the domain.
My rules:
- Round at the boundary (input or output), not repeatedly in the middle.
- If you must round in the middle, do it intentionally and document why in the test.
- For money-like values, keep integers in the smallest unit.
Example: converting user input to cents
This is the pattern I use for “type 12.34 in a form, store 1234 in state.”
function dollarsToCents(raw) {
const n = Number(raw);
if (!Number.isFinite(n)) return null;
return Math.round(n * 100);
}
This isn’t perfect for every locale or currency, but it’s vastly safer than keeping floating totals.
Example: percentages that must sum to 100
If you’re allocating percentages across categories, naive rounding can make totals drift.
Instead of rounding each part independently, I usually:
1) compute in basis points (hundredths of a percent) or thousandths,
2) allocate remainder deterministically.
function allocatePercents(weights) {
const total = weights.reduce((a, b) => a + b, 0);
if (total === 0) return weights.map(() => 0);
const scaled = weights.map(w => (w / total) * 10000); // basis points
const floors = scaled.map(x => Math.floor(x));
let remainder = 10000 – floors.reduce((a, b) => a + b, 0);
const order = scaled
.map((x, i) => ({ i, frac: x – Math.floor(x) }))
.sort((a, b) => b.frac – a.frac);
const out = […floors];
for (let k = 0; k < remainder; k++) out[order[k].i] += 1;
return out.map(bp => bp / 100); // back to percent
}
That’s a lot of code for “rounding,” but this is exactly the kind of rounding bug that shows up in dashboards and finance-ish UIs.
BigInt in Practice: Great Power, Very Specific Rules
BigInt solves a real class of production problems, but you have to accept its constraints.
The constraints I plan around
- No mixing with
Numberin arithmetic. - No decimals.
- JSON can’t serialize BigInt directly.
- Some standard library APIs expect
Number(for example, manyMath.*functions).
Converting safely
If I’m converting BigInt to Number, I do it only if I can prove it fits.
function bigIntToSafeNumber(b) {
if (b > BigInt(Number.MAXSAFEINTEGER) || b < BigInt(Number.MINSAFEINTEGER)) {
return null;
}
return Number(b);
}
If I’m converting Number to BigInt, I avoid implicit rounding decisions:
function toBigIntFromIntegerNumber(n) {
if (!Number.isInteger(n)) return null;
if (!Number.isSafeInteger(n)) return null;
return BigInt(n);
}
BigInt division truncates
This matters for average calculations and rates.
console.log(5n / 2n); // 2n
console.log(-5n / 2n); // -2n (toward zero)
If I need a rational, I keep numerator and denominator separately, or I switch to Number with explicit rounding.
Practical Scenarios: Arithmetic You Actually Write
These are the places I see arithmetic operators used every day—and the failure mode I look for.
Pagination offsets
Off-by-one and negative page numbers are classic.
function pageOffset(pageIndex, pageSize) {
if (!Number.isInteger(pageIndex) || pageIndex < 0) return null;
if (!Number.isInteger(pageSize) || pageSize <= 0) return null;
return pageIndex * pageSize;
}
Here, * is boring; validation is the real feature.
Time windows and rate limiting
A common pattern is “requests per second/minute.” Division by zero happens when the window is 0 (bad config) or when two timestamps are identical.
function perSecond(count, durationMs) {
if (!Number.isFinite(count) |
durationMs <= 0) return null;
return count / (durationMs / 1000);
}
Progress bars
These are tiny but user-visible. I clamp and guard.
function progress(current, total) {
if (!Number.isFinite(current) |
total <= 0) return 0;
const raw = current / total;
const clamped = Math.min(1, Math.max(0, raw));
return clamped;
}
Discounts and markups
The operator that causes pain here is still + because inputs arrive as strings.
function applyDiscountCents(priceCents, discountPercent) {
if (!Number.isInteger(priceCents) || priceCents < 0) return null;
if (!Number.isFinite(discountPercent) |
discountPercent > 100) return null;
const discountBp = Math.round(discountPercent * 100); // basis points
const discounted = Math.round((priceCents * (10000 – discountBp)) / 10000);
return discounted;
}
Performance Considerations: Arithmetic Is Cheap, Conversions Aren’t
In most apps, arithmetic itself is not the bottleneck. What slows things down is:
- parsing the same string repeatedly,
- creating lots of temporary objects while mapping/formatting,
- running
Number(...)inside tight loops, - doing unnecessary rounding per item.
So my performance advice is boring:
- Convert once at the boundary.
- Store clean numbers in state.
- Use integer units internally.
A simple example is computing totals for a list.
Instead of:
// Bad pattern: parsing repeatedly
const total = items.reduce((sum, item) => sum + Number(item.price), 0);
I’d do:
// Better pattern: parse when loading items
const normalizedItems = items
.map(item => ({ …item, price: Number(item.price) }))
.filter(item => Number.isFinite(item.price));
const total = normalizedItems.reduce((sum, item) => sum + item.price, 0);
A 2026 Checklist I Use When Reviewing Arithmetic Code
When I review PRs with arithmetic, I look for these patterns.
- Boundary parsing: Is user input parsed once and validated?
+ambiguity: Is there any chance of string concatenation?NaNpropagation: CouldNaNleak into later calculations?Infinitybehavior: Could division by zero silently “work”?- Rounding policy: Where do we round, and is it tested?
- Units: Are we mixing cents and dollars, seconds and milliseconds?
- BigInt boundaries: Are conversions explicit and safe?
++/--in expressions: Are we relying on evaluation order?
If I find one of these, I don’t just ask for a code tweak. I ask for a test case that would have failed before.
Key takeaways and what I’d do next
Arithmetic operators are simple until data stops being clean. The moment your values come from a form, a URL, a CSV, or another service, the operator rules around coercion, NaN, and Infinity become part of your application logic whether you wanted them or not.
If you want a practical next step, here’s what I’d do in an existing codebase:
- Add a tiny set of parsing helpers (like
toFiniteNumber) and use them at boundaries. - Convert money-like values to integer smallest units early.
- Add tests for empty strings, whitespace strings,
null,undefined, negative values, and extremely large integers. - Add lint rules (or team conventions) that discourage
++/--inside larger expressions.
When arithmetic is boring, everything downstream gets easier: UI stays consistent, analytics stops drifting, and “impossible” totals don’t show up in your logs at 2 a.m.


