Crabrix blog
Rust numeric safety: checked arithmetic and honest conversions
Choose explicit Rust overflow and conversion policies. Compare checked, wrapping and saturating arithmetic, handle narrowing, and test numeric boundaries.
· 8 min read
Crabrix blog
Choose explicit Rust overflow and conversion policies. Compare checked, wrapping and saturating arithmetic, handle narrowing, and test numeric boundaries.
· 8 min read
A counter can have the correct integer type and still behave incorrectly at its boundary. A conversion can compile and still discard information the application needed. Rust gives these operations defined rules, but the rules do not automatically match a product’s intent. A completed-task count, circular sequence number and brightness control can reasonably need three different answers to the same overflowing sum.
This guide starts with a byte-sized calculation so every result can be checked by hand. We then separate conversion from arithmetic, examine signed and unsigned values, and build a small checked calculation. The programs use only the standard library. They are useful in Crabrix and in larger desktop projects because the central question is the same: which values are valid, and how should an invalid result be reported?

A u8 represents integers from zero through 255. Adding ten to 250 produces the mathematical result 260, which does not fit. checked_add returns None; wrapping_add reduces the result modulo 256 and returns four; saturating_add clamps it to 255. None of these is the universally correct product behavior. The choice depends on what the stored number means and what a caller needs to know.
The example makes all three policies executable. Printing an option with debug formatting is a convenient way to observe None, while assertions state the expected results independently of the output. Do not treat saturation as error reporting: after clamping, the result no longer tells you how far beyond the limit the mathematical answer was. Similarly, wrapping preserves modular behavior but loses the original magnitude. Select the policy before using its name in code.
fn main() {
let value = 250_u8;
assert_eq!(value.checked_add(10), None);
assert_eq!(value.wrapping_add(10), 4);
assert_eq!(value.saturating_add(10), 255);
println!("checked: {:?}", value.checked_add(10));
println!("wrapping: {}", value.wrapping_add(10));
println!("saturating: {}", value.saturating_add(10));
}Ordinary integer arithmetic can panic on overflow when overflow checks are enabled. Common development and release profiles use different defaults, and those settings can be changed. A statement such as “debug always panics and release always wraps” overstates what a build profile guarantees. Constant evaluation has its own restrictions too. If the application needs a stable policy, express that policy through the appropriate operation rather than relying on a profile name.
This distinction prevents a particularly confusing defect: a test run appears to reject an excessive count, while another build silently produces a wrapped value. Named methods make the intended behavior visible in both places. The infographic compares the exact byte-sized results from our example and separately shows a narrowing conversion. It does not present a performance ranking or imply that a conversion and an addition are the same operation.

Sources: Standard library: checked, wrapping and saturating integer operations · Cargo reference: overflow checks in profiles
An integer narrowing cast with as has defined behavior. Casting 300 to u8 retains the low eight bits and produces 44. That can be a logical error for an inventory count, but it does not require an unsafe block and is not automatically memory unsafety. Keeping these terms separate matters when learning Rust: “inappropriate for this domain” and “violates a memory-safety requirement” describe different problems.
Use a fallible conversion such as u8::try_from when out-of-range input should be rejected. Use From when the particular conversion is guaranteed to succeed. The choice communicates information to a caller: either a valid destination value is produced, or the caller receives a conversion failure to handle. Do not solve a compiler type mismatch by adding as before examining whether the source’s negative values or larger range are meaningful.
fn main() {
let large = 300_i32;
assert_eq!(large as u8, 44);
assert!(u8::try_from(large).is_err());
assert!(u8::try_from(-1_i32).is_err());
assert_eq!(u16::from(250_u8), 250);
println!("Conversion checks passed");
}Sources: Standard library: TryFrom
A report total may multiply a unit price by a quantity and then add a fee. Checking only the final addition leaves the multiplication unexamined. If an intermediate result overflows before the final check, the program has already lost the correct mathematical value. Break the expression into stages and choose a policy for each stage that can exceed the representation’s range.
The function below reports the stage that failed. Its inputs are illustrative integer units, not a complete financial model: it does not define currency rounding, taxation or decimal precision. That limitation keeps the example’s contract precise. The useful pattern is chaining checked operations while preserving enough context to distinguish multiplication failure from addition failure. Callers can then decide whether to ask for different input, use a wider representation or reject the operation.
fn total(unit: u32, quantity: u32, fee: u32) -> Result<u32, &'static str> {
let subtotal = unit.checked_mul(quantity).ok_or("subtotal overflow")?;
subtotal.checked_add(fee).ok_or("fee overflow")
}
fn main() {
assert_eq!(total(12, 3, 2), Ok(38));
assert_eq!(total(u32::MAX, 2, 0), Err("subtotal overflow"));
assert_eq!(total(u32::MAX, 1, 1), Err("fee overflow"));
println!("Checked total: {}", total(12, 3, 2).unwrap());
}An unsigned type excludes negative values, but choosing it does not make every conversion from signed input valid. A negative temperature or offset may be meaningful in the source domain. Casting it into an unsigned type changes its representation according to Rust’s rules; it does not establish that the original value was acceptable. Validate the domain and use a conversion that preserves the intended failure behavior.
Signed arithmetic also has asymmetric endpoints: the minimum signed value has one more unit of magnitude than the maximum positive value. Operations such as negation, absolute value or division of the minimum value by negative one deserve explicit boundary consideration. You do not need to memorize every edge immediately. Read the operation’s documented checked variant and add a fixture for the exact boundary your function can encounter, especially when accepting values from outside the program.
Integer division truncates toward zero. That is useful when the application wants whole units, but it can surprise someone expecting a fractional average. A zero divisor is another separate condition, and signed division can also encounter a representability boundary. A checked division method can express failure without depending on a panic as ordinary control flow. Decide what the caller should see when a division cannot produce a supported result.
Converting to floating point changes the problem rather than universally solving it. Floating-point operations have their own precision and special-value rules. A display that needs an approximate average can reasonably choose a floating type; a protocol field requiring an exact integer cannot silently adopt that choice. State whether rounding, truncation or a remainder is part of the intended result. Tests should verify the rule with values that do not divide evenly, not only convenient multiples.
usize is appropriate for collection lengths and indexes because its width follows the target’s pointer width. That does not make it an ideal serialized file count or cross-platform protocol integer. An on-device WASI target and a desktop host can use different widths. If a file format specifies a fixed-width integer, keep that representation explicit and validate conversions when moving into an indexing operation.
The same reasoning applies to counts that might exceed available memory. A valid serialized count does not prove that allocating that many elements is possible or acceptable. Validate limits before allocation, and distinguish a format’s maximum from the product’s supported maximum. The goal is not to make every number larger. It is to make representation changes visible at the boundaries where their assumptions can be checked.
Replacing a failed conversion or checked operation with zero can make the program continue while silently changing its meaning. That is acceptable only when zero is the documented fallback. For many counters, a clear error is better than an apparently valid but fabricated result. Use an error type with enough structure for the caller to distinguish unsupported input, arithmetic overflow and an unavailable external value.
Also avoid logging whole sensitive records just to explain a failed calculation. A field name, operation stage and bounded numeric context may be enough. Preserve the original input where recovery requires it, and avoid partially updating persistent state before validation succeeds. Numeric safety includes what the application does after failure, not merely whether a helper returned None or Err somewhere along the path.
A type range cannot tell whether a number represents seconds, milliseconds or a count of records. Adding two values with the same primitive type can still be a domain mistake when their units differ. Clear names help, and small wrapper types can make important distinctions explicit at API boundaries. Validate representation and units as separate requirements.
For example, converting a supported duration into a smaller display unit needs a rounding rule as well as an overflow rule. Multiplying before dividing can overflow even when the final mathematical result would fit. Reordering operations can change truncation. Write the intended formula and units first, then choose checked stages or a representation that can express the required intermediate values.
Floating-point values represent many real-world measurements approximately. Converting an integer calculation into floating point does not provide unlimited exact arithmetic, and a successful conversion does not guarantee every possible integer remains distinguishable. Choose an error tolerance from the measurement or algorithm when approximate comparison is required; an arbitrary small constant is not a universal solution.
Also decide how the application handles non-finite results and invalid input before displaying a total or storing a measurement. These concerns are separate from integer overflow, but both illustrate why a numeric type alone cannot specify the whole contract. Keep examples for ordinary values and the actual failure boundaries the product accepts rather than relying on a few convenient calculations that happen to be exact.
For each numeric operation, test an ordinary result, the largest accepted result and the first rejected result where practical. For a conversion, include a negative source when the destination is unsigned and an excessive positive source when narrowing. For a multi-stage calculation, create separate cases that fail at each stage. Assertions should describe the expected domain result, not simply confirm that execution did not panic.
If an operation updates a model, verify that rejection leaves the prior model intact. If it clamps intentionally, test the clamped value and document the loss of magnitude. If it wraps, test the modular result explicitly. A short review can then answer four questions: what range is valid, where conversion occurs, which overflow policy applies, and what a caller retains after failure. Those answers make numeric code dependable across builds and targets.