Rust Integer Overflow: Choosing the Right Arithmetic Contract

Rust Integer Overflow: Choosing the Right Arithmetic Contract

Table of Contents

I wrote an article on integer arithmetic overflow contracts to explain the different ways an integer overflow can be handled, and how programming languages differ in the way they handle it by default.

This article is the Rust-specific version of that idea: the same underlying overflow model can be chosen explicitly, and Rust makes that choice visible in the method names.

By default, Rust checks for integer overflow in debug builds and wraps on overflow in release builds. This setting can be changed through the build configuration, so the build profile alone is not a reliable guide to the behavior of +.

I am not particularly fond of this behavior, because it can lead to subtle bugs in production code if you are not careful. I prefer to be explicit about the overflow behavior that is appropriate for my use case.

Rust provides a rich set of arithmetic methods that let you choose how an operation handles overflow. This article compares what each method returns and what it guarantees when overflow occurs.

​
Integer Overflow Contracts

We’ll use addition (+) to compare the main overflow contracts. Rust provides related methods for other operations, but their availability depends on the operation and integer type.

​
Wrapping around

The wrap-around model keeps the value within the integer range by modular arithmetic.

Rust exposes this in two related methods:

1fn wrapping_add(self, rhs: Self) -> Self
2fn overflowing_add(self, rhs: Self) -> (Self, bool)

The difference is important:

  • .wrapping_add() returns only the wrapped result.
  • .overflowing_add() returns the same wrapped result together with a boolean indicating whether overflow happened.

Example:

1let value: u8 = 255;
2
3let wrapped = value.wrapping_add(1); // 0
4let (wrapped2, overflowed) = value.overflowing_add(1); // (0, true)

The numeric result is the same in both cases. The difference is observable through the API: .wrapping_add() does not report overflow, while .overflowing_add() exposes it as a boolean.

Use .wrapping_add() when you only need the wrapped value. Use .overflowing_add() when you also need to know whether overflow happened.

For built-in integer types, + wraps on overflow when overflow checks are disabled, just like .wrapping_add(); unlike +, .wrapping_add() guarantees wrapping in every build configuration.

​
Saturating Arithmetic

The saturation model clamps the value at the boundary instead of wrapping.

1fn saturating_add(self, rhs: Self) -> Self

Example:

1let value: u8 = 255;
2let clamped = value.saturating_add(1); // 255

Saturating arithmetic is useful when a value should stay at its boundary rather than wrap, for example, a UI control constrained to a fixed range.

​
Checked Arithmetic

The checked model asks: “can this operation be represented?”

1fn checked_add(self, rhs: Self) -> Option<Self>

Example:

1let value: u8 = 255;
2let result = value.checked_add(1); // None

This serves a similar purpose to recoverable exception handling: the caller can choose a separate path for overflow. Rust represents that outcome explicitly as None. For example, match on Some(sum) to use the result and on None to reject the input or take another recovery path.

You may ask: “Why not just use .overflowing_add() and check the boolean?” The answer is that .checked_add() expresses the intent directly: the caller receives None when the operation cannot be represented.

​
Panic and Fail-Stop Contracts

With the default settings, overflowing + panics in debug builds. More generally, a panic-on-overflow contract is useful when overflow violates an invariant: if the result cannot be represented, execution stops instead of continuing with a wrapped value.

Nightly or experimental builds may provide a stricter fail-fast arithmetic API such as strict_add(), but the stable Rust pattern remains explicit handling through .checked_add() and expect(...) when you want a panic on failure.

This is the stable Rust equivalent of a panic contract:

1let result = value.checked_add(other).expect("overflow");

This is equivalent in intent to the panic model: the code refuses to continue when the operation cannot be represented, but it does so in a way that is explicit, readable, and easy to control.

​
Unsafe Arithmetic and Undefined Behavior

This is not a safe Rust operation, but it is available through the unsafe method .unchecked_add().

1unsafe fn unchecked_add(self, rhs: Self) -> Self

Example:

1let result = unsafe { 10_u8.unchecked_add(1) }; // 11; the precondition is satisfied
2let value: u8 = 255;
3let result = unsafe { value.unchecked_add(1) }; // Undefined behavior; the precondition is not satisfied

Use .unchecked_add() only when you can prove the sum always fits, and benchmarks show a benefit in performance-critical code. Otherwise, use .checked_add() when overflow needs handling, or + when the sum is guaranteed to fit. Violating the precondition causes undefined behavior.

In this respect, .unchecked_add() resembles signed integer + in C language: the compiler may assume overflow does not occur, and violating that assumption is undefined behavior.

​
The General Operation-Family Pattern

Rust often names arithmetic methods by combining a contract with an operation: wrapping_add, checked_mul (checked multiplication), and overflowing_shl (overflow-reporting left shift). But not every contract is available for every operation and integer type.

For example, signed integer types provide saturating_neg and unchecked_neg, but unsigned integer types do not. Unsigned types provide checked_neg, which succeeds only for zero. Shift operations have checked, wrapping, overflowing, and unchecked variants, but no saturating shift methods. These methods govern the shift amount; for example, checked_shl returns None when the amount is at least the integer width, not when nonzero bits are shifted out.

Check the documentation for the specific type and operation rather than assuming every contract is available.

​
Choosing an Overflow Contract

  • Use wrapping arithmetic when modular results are intended.
  • Use overflowing arithmetic when you need both the wrapped result and an overflow flag.
  • Use checked arithmetic when overflow should be represented as None and handled by the caller.
  • Use saturating arithmetic when results should clamp at the type’s numeric boundary.
  • If overflow violates an invariant, turn a failed checked operation into a panic, for example with expect(...). Use unchecked arithmetic only when its safety precondition is guaranteed.

Tip

Rust also handles explicit overflow for floats.

It provides .is_nan() and .is_finite() methods to check for these conditions.

Are you learning Rust? Check out this curated list of resources and coding conventions here.

Share :

Related Posts

Why Backoff Matters: A Retry Storm Can Kill a Service

Why Backoff Matters: A Retry Storm Can Kill a Service

A zero-backoff retry loop can turn one failed API call into a CPU and load spike that destabilizes the whole service. This article explains why, with …

Read More about Why Backoff Matters: A Retry Storm Can Kill a Service
Make Numbers More Readable in Your Code

Make Numbers More Readable in Your Code

Have you ever seen a giant number in your code, like 100000000, and thought, What even is this? I explored 50 top programming languages to see which …

Read More about Make Numbers More Readable in Your Code
Rust `match` Tips: Handling Vectors by Length

Rust match Tips: Handling Vectors by Length

You’re writing a Rust function that takes a Vec<T> and depending on how many elements are in it (say 1 to 4), you want to do different …

Read More about Rust `match` Tips: Handling Vectors by Length