You are setting the arithmetic policy for a codebase that does size, quota and money calculations on untrusted numbers across several languages. How do you decide between checked (fail-fast), saturating, wrapping and wider-type arithmetic, and where do you put the guarantee?
answer
- policy follows the sink, not the operator
- checked = default for size/money/quota
- saturating is right only when clamping is the meaning
- wrapping must be named, not implicit
- wider type = range argument; 2^53 at the JSON hop
basics
~20 sDefault to checked arithmetic that fails fast wherever the value authorises something. Allow saturating only where a clamped value is semantically correct, such as display or back-off caps. Wrapping is opt-in and must be named. Wider types are a range argument, never a fix. Put the guarantee in shared helpers and types, not in review discipline.
solid answer
~60 sDecide by asking what a wrong value would authorise. - **Checked / trapping** is the default for any quantity that gates memory, money or entitlement. It is the structural-separation rung: the wrong value never reaches the sink, and failure is loud and local. - **Saturating** is legitimate only where clamping is the *correct semantic* — a progress indicator, a back-off ceiling, a bounded score. It is catastrophic for allocation sizes, because clamping to the maximum converts a wrap into an enormous allocation, and misleading for money. - **Wrapping** belongs to hashes, checksums and ring buffers and must be spelled out at the call site so a reader sees intent rather than an accident. - **Wider types** are an argument about range, and only valid if you can state the bound on the whole expression. They also create new boundaries: 64-bit identifiers crossing a JSON/JavaScript hop lose exactness past 2^53 — no wrap, silent rounding. Make it structural: money gets a decimal type, sizes get a checked helper, and the raw operator on untrusted quantities is what looks wrong in review. Back it with sanitizers and fuzzing as the detection rung, never as the primary control.
go deeper
Say checked arithmetic that fails fast is the default for sizes and money, and that saturating quietly keeps a wrong value.
Contrast the four modes with a correct use for each, and explain why clamping an allocation size is worse than rejecting it.
Organise by sink band, put the guarantee in shared helpers and types, and place sanitizers explicitly at the detection rung.
Write the policy as a guarantee required at the sink so each language satisfies it its own way, and define how compliance is measured in CI rather than in review.
## Start from the sink, not the operator The policy question is not "which arithmetic is safest" but "what does this number decide". Group the codebase's arithmetic into three bands: - **Authority-bearing**: allocation sizes, bounds and offsets, money and fees, quotas and balances, entitlement counts, expiry and validity windows, retry and lockout counters. - **Structural**: indices, capacities and internal bookkeeping that is derived from already-validated values. - **Cosmetic**: metrics, progress, display, telemetry. Only the first band justifies a hard policy. Applying one everywhere burns credibility and produces a wall of noise that teams learn to bypass. ## The four modes and what each buys **Checked / trapping.** The operation either yields the mathematically correct value or signals failure — an error return, an exception, a panic. This is the **structural separation** rung of the defence ladder: the wrong value is unrepresentable downstream. The cost is that every call site must handle a failure path, which is precisely the design pressure you want on authority-bearing arithmetic, because it forces the author to decide what "this request is absurd" means. Reject at the boundary with a clear error; do not clamp and continue. **Saturating.** Clamps at the type's limits. It sits on the **transformation** rung, and the reason it is strictly below checked arithmetic is worth stating: saturation still produces a *wrong* value — it merely makes the wrongness monotonic instead of discontinuous. That is exactly right for a back-off delay or a rendered bar, where "as much as possible" is the intended meaning. It is exactly wrong for an allocation size, where the clamp turns a wrap into a maximal allocation and a memory-exhaustion denial of service, and for money, where clamping silently changes the amount a customer owes. **Wrapping.** Correct and necessary for hashes, checksums, PRNGs, ring-buffer indices and sequence numbers that are defined modulo something. The policy is not to forbid it but to require it be *named* — a wrapping-add function or an explicitly documented type — so that a reader can distinguish deliberate modular arithmetic from an accident. Silent wrap as the language default is the problem; wrap as a stated intent is fine. **Wider types.** Widening is a *range argument*: it is valid only when you can state and defend the bound on the entire expression, including every future term. It is a legitimate technique — compute in a type that provably holds the product of the validated operand ranges, then validate the result once and narrow — but it is not a control, because nothing enforces the bound after the next edit. It also introduces its own boundaries: a 64-bit identifier or amount that crosses a JSON or JavaScript hop becomes a double and loses exactness above 2^53, so two distinct identifiers can compare equal with no wrap and no error. Cross-language numeric boundaries deserve the same scrutiny as the arithmetic itself; where they exist, carry large integers as strings. ## Where the guarantee lives A policy that lives in a wiki is a detection control at best. Push it into the code: - **Types**: money as a decimal or minor-unit type with checked operations rather than a raw integer; sizes as a dedicated type that only supports checked operations. - **Helpers**: one `checked_add` / `checked_mul` / `range_within` per codebase, so the correctness proof exists once and every call site inherits it. - **Build settings** where the language offers them: overflow checks enabled in the modes you actually ship, or a documented decision that they are not, with the reasoning recorded. Note that defining wraparound (rather than leaving it undefined) removes the compiler-deletes-your-check hazard but keeps the wrong value — it is a correctness improvement, not a defence. - **Detection, last**: overflow sanitizers in CI, fuzzing the parsers that consume declared lengths, property tests asserting that computed sizes match what is actually written. This is the bottom rung — it finds instances and guarantees nothing, so it must never be the primary control. ## The polyglot problem Different runtimes give the same source construct different meanings: defined wraparound, undefined behaviour, debug-only panics, arbitrary precision that fails as memory exhaustion instead, and floating-point that rounds rather than wraps. A single global rule cannot hold. Write the policy in terms of *guarantees required at the sink* — "a size that reaches an allocator must have been produced by an operation that cannot silently return a wrong value" — and let each language satisfy it with its own mechanism. Then the review question is uniform even though the implementations differ. ## What good looks like A reviewer opening a diff should be able to see, from the shape of the code alone, that an authority-bearing quantity was computed safely, and a raw operator on untrusted input in that band should look conspicuous. If deciding requires re-deriving the algebra, the policy has failed regardless of how correct the individual expressions currently are.
- Why not just enable trapping arithmetic globally and be done?Because it converts every latent wrap into a crash, including in cosmetic and third-party code, and because modular arithmetic is genuinely correct for hashes, checksums and ring buffers, which would then need per-site exemptions anyway. Enable it where the runtime supports it cheaply, but the durable answer is types and helpers on authority-bearing arithmetic plus an explicit wrapping operation where wrap is intended.
- How do you decide the policy is working rather than just written down?Measure whether authority-bearing arithmetic actually flows through the checked types and helpers — that is greppable and can be enforced in CI — and whether new code follows without prompting. Sanitizer and fuzzing findings are the trailing indicator; a rising count there means the structural controls are not being reached, not that detection is succeeding.
saying these in an interview costs you the question
- Prescribing saturating arithmetic as "safe by default", which is disastrous for allocation sizes and wrong for money.
- Treating a wider type as a fix rather than as an argument about range.
- Applying one blanket rule across languages with genuinely different overflow semantics.
- Relying on sanitizers and fuzzing as the primary control instead of the last rung.
- Forgetting the cross-language numeric boundary, where a 64-bit value becomes a double and loses exactness with no wrap at all.