Operators of death: checked arithmetic in Rust
From The Rust Spellbook
Everything has its limits, even Rust, and this applies to numeric types too. They always have a fixed width—a fixed number of bits—and that means there’s a limit to how large a number they can represent. When the result of a sum is bigger than that maximum value for the type, we can no longer represent it correctly: a problem called overflow.
Spells of protection from arithmetic overflow
You probably know that numbers (and everything else) are represented
internally in the computer using binary bits. Rust’s
various numeric data types each have a fixed number of bits with which
to represent a number: for example, the u8
type represents a number using 8 bits. That’s because the compiler needs
to know how much memory space to reserve for each of the data values in
your program.
But just as there’s only so large a number you can write down in decimal on a fixed-width piece of paper, there’s only so large a number you can store as a Rust value using a fixed number of bits. Let’s see if we can figure out what that number is.
Thinking in bits
The relationship between the width of a data type—the number of bits it uses to store a value—and the range of values it can represent is straightforward, but let’s think our way into it a little.
Suppose we had only one bit available to represent numbers. That would be tough, but we could still do something, and it’s the easiest case to start with.
A single binary bit can take one of two possible values (hence “binary”). It’s up to us what names we use for these values: “true or false”, “on or off”, “set or clear”, “high or low” are all used in various contexts. But for this discussion let’s call them 0 or 1.
How many different number values can we represent with a 1-bit type? The answer is two. Easy, right? The choice of which decimal numbers these two values represent is purely arbitrary. It doesn’t have to be 0 and 1, but let’s go with that for simplicity.
Now suppose we widen our type a little, to two bits. How many possible values can we represent now? Maybe you see the answer right away, but let’s write out all the possibilities to be clear:
| Bit pattern | Decimal |
|---|---|
| 00 | 0 |
| 01 | 1 |
| 10 | 2 |
| 11 | 3 |
Again, the precise decimal numbers that we assign to each permutation of the bits are an arbitrary choice, and we might choose differently depending on the situation.
With the scheme we’ve outlined so far, we can only represent zero or positive numbers (called unsigned representation), but that’s okay; there are other schemes. Let’s see how this one changes as we add more bits to the representation.
Widening the type
How about a three-bit type? Can you say what the cardinality—the number of possible values—is now? If you guessed “six”, I totally see where you’re coming from, but think about it like this: a one-digit decimal number can represent 10 values, while a two-digit decimal number can represent 100 values. Each extra digit we add doesn’t add ten to the cardinality, it multiplies it by ten, because the new digit creates ten possible variations of each existing number.
And it’s the same principle here: each binary digit we add multiplies the cardinality by two, because that digit can take two values. For example, you could take each of the four 2-bit values from the previous table, and put a “0” bit in front of them, and then you could take them all again and put a “1” bit in front. Thus, the extra bit gives us twice the possibilities: that’s 8 values.
How many numbers can we represent using 8 bits, then—that is, in a
Rust u8? With one bit, we had 2 possibilities, with two
bits, 4, and with three, 8, doubling with each extra bit. So we’ll
continue, muttering under our breath, “four bits gives 16, five bits
gives 32, six bits gives 64, seven bits gives 128, and eight bits gives
256.”
Okay, so we have 256 possible bit patterns, and if we interpret them
as unsigned decimal numbers (“u8” means “unsigned, 8
bits”), each one can represent a value between 0 and 255 inclusive.
Obvious question, then: what happens if we try to put a number bigger
than 255 into a u8?
Oh no, overflow
Let’s write a simple Rust program where we do just that. For example:
fn main() {
let val: u8 = 255;
println!("{}", val + 1);
}If we have a u8 variable containing 255, and we try to
add 1 to it, then we feel convinced that something interesting
should happen. Let’s see what:
cargo run
error: this arithmetic operation will overflow
--> src/main.rs:3:20
|
3 | println!("{}", val + 1);
| ^^^^^^^ attempt to compute `255u8 + 1_u8`,
| which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default
Rust won’t let us even build this program, because it’s clear at compile time that the result of the sum won’t fit in 8 bits. That’s helpful, but Rust can’t protect us from every instance of this kind of mistake. It’s not always so clear to the compiler that overflow will happen.
Would you say it’s time to panic?
For example, instead of adding a constant, suppose we use a loop to add increasingly larger numbers to some starting value:
fn main() {
let val: u8 = 250;
for i in 1..7 {
let answer = val + i;
println!(" + = ");
}
}We can see that this will soon exceed 255, but I think we’ll be able to sneak that fact past Rust. Let’s find out:
cargo run --example overflow
Here’s the output:
250 + 1 = 251
250 + 2 = 252
250 + 3 = 253
250 + 4 = 254
250 + 5 = 255
thread 'main' panicked at examples/overflow.rs:4:42:
attempt to add with overflow
Okay. Rust couldn’t catch the problem at build time in this case, but it caught it at run time, which is still better than nothing. At least we didn’t get the wrong answer.
Hang on, though. By default, cargo run builds our
program using the dev
profile, which behaves slightly differently to the release
profile. Let’s try it with release instead to see what
happens:
cargo run --profile release --example overflow
250 + 1 = 251
250 + 2 = 252
250 + 3 = 253
250 + 4 = 254
250 + 5 = 255
250 + 6 = 0
Oh no! It seems like Rust can’t do this simple sum correctly. Before you apply to the Rust Foundation for a refund, though, bear in mind this is documented behaviour, even if it may not be what we intended.
Rust only promises to check for overflow in dev builds. In release builds, these checks are disabled, and the overflowing value wraps: that is, it starts again at the lowest possible value, which in this case is zero.
I care about this because…?
Wrapping an overflowed result is a pragmatic policy, but not always the right one. Imagine yourself on the treatment table of a radiation therapy machine, for example. And let’s say a subtle bug in the dose-calculation software leads to an arithmetic overflow.
Now, what would you prefer to happen next? Choose from one of the following:
- The machine stops with an error, requiring the operator to reset it.
- The machine gives you an incorrect, fatal dose of radiation.
I can’t imagine any programmer would rather kill the patient than see an error message but, surprisingly, that’s Rust’s default behaviour in this case. (Ironically, my computer crashed right while I was typing that sentence. Food for thought next time you’re trusting your life to someone else’s software.)
Programmers have a term for features like this that make sense in context, but are also easy to misuse or misunderstand, and dangerous when you do: they’re called footguns. In other words, they make it possible to shoot yourself in the foot.
The fact that numbers wrap around in Rust when the +
operator overflows is just such a footgun. What can we do to avoid a
negligent discharge, then? We could try to avoid doing arithmetic at all
in our programs, but that’s rather limiting. Is there a better
option?
Setting
overflow-checks for release builds
As we’ve seen, with the dev profile Rust will catch
overflows for us and stop the program. If you want overflows to be
checked with release too, or with any other build profile
you define, you can configure the overflow-checks
setting in your Cargo.toml:
[profile.release]
overflow-checks = trueThis is the quickest and simplest way to avoid overflow in
production, but it’s not necessarily the best way. For one
thing, it’s not clear to someone reading your program that overflows
will be checked in release mode, if they don’t happen to also glance at
your Cargo.toml. It’s not the default behaviour, so readers
won’t be expecting it.
Also, overflow checks aren’t free. They add significant extra size and run time to the compiled program, and we don’t always need them, so it’s a shame to pay this performance penalty for all arithmetic operations when we don’t have to.
By the way, it’s not only the + operator you need to
watch out for. The * operator can overflow too, since
multiplication is just repeated addition.
A similar problem can happen with the - operator: if the
result of the subtraction is less than the minimum value for the type,
it’ll wrap back around to the maximum value (this is called
underflow, but it’s the same problem: we ran out of
bits).
The /, % (remainder), <<
(shift left) and >> (shift right) operators can
also overflow on integer types. That’s a whole arsenal of footguns
waiting to give us (or our users) a nasty surprise.
The operators… of death
Assuming we care about getting our sums right, then, what should we do about this sort of thing? Take a firm grip on your web browser, because I’m about to drop a controversial hot take:
We should avoid using the + operator in
Rust.
While you’re still reeling from that, I think we should avoid the
-, *, /, %,
<< and >> operators too. No matter
how wide the data type we use with them, these operators can cause
overflow and thus lead to unexpected or non-obvious behaviour.
How on earth are we supposed to do arithmetic in Rust if we shouldn’t use these operators? Well, we have other ways of doing arithmetic. One of the major things Rust does to help us write correct programs is to give us ways to be more explicit about their behaviour.
Arithmetic is a good example of this, because it turns out we can choose exactly the overflow behaviour we want for any particular sum, regardless of the build mode. Let’s go through the available choices one by one.
The checked_add method
checked_add
returns an Option,
which is Rust’s usual way of indicating that there may or may not be a
value here.
In this case, if everything’s okay, we’ll get
Some(answer), but if the result of the addition overflowed
the type, we’ll get None instead:
print!(" + = ");
if let Some(answer) = val.checked_add(i) {
println!("");
} else {
println!("oh no, overflow!");
}This is almost certainly what we want in most cases. We’re saying
that we don’t expect this sum to overflow, but it might. If it
does, the answer will be invalid , so we don’t want it (we’ll get
None). But if everything goes according to plan, we’ll get
Some(answer), and that tells us answer is
valid: there was no overflow.
Here are the checked-arithmetic equivalents for the other operators:
| Operator | Checked equivalent |
|---|---|
- |
checked_sub |
* |
checked_mul |
/ |
checked_div |
% |
checked_rem |
<< |
checked_shl |
>> |
checked_shr |
And there are many more checked_X methods for other
arithmetic operations; have a look at the documentation for u8,
for example, to see the full list.
The overflowing_add
method
Sometimes, more rarely, we expect that the sum may overflow, and if it does, the answer isn’t necessarily invalid. All the same, we need to know whether overflow has happened, because it may mean that we treat the answer differently.
In this case, we can use overflowing_add.
Instead of an Option, this returns a tuple of two elements:
the answer (wrapped or not), and a bool value that tells us
whether overflow occurred:
let (answer, overflow) = val.overflowing_add(i);
println!(
" + = {}",
if overflow {
"(overflowed and wrapped)"
} else {
"(no overflow)"
}
);Again, there are overflowing_X equivalents for the other
operators, and the same is true for all the methods we talk about
here.
The wrapping_add
method
Sometimes we always want the number to wrap on overflow, and
we don’t need to know whether or not this happened. That’s what the wrapping_add
method gives us:
let answer = val.wrapping_add(i);Using wrapping_add makes sense when we want the
answer to wrap around to its lowest value once it goes past its highest
possible value. That’s rare, but there are cases where it’s useful: some
kinds of counter or timer use this behaviour, for example.
Making the wrapping explicit helps the reader understand that overflow is expected here, and that it’s the correct behaviour for this particular sum.
The saturating_add
method
Sometimes, rather than wrap the value or return None, we
want to constrain the result to the maximum value
allowed for the type. That’s the behaviour we get from the saturating_add
method:
let answer = val.saturating_add(i);With our overflow-testing example, this gives:
250 + 6 = 255
Obviously this is the wrong answer to this particular sum, but by
using saturating_add we’re saying that we want
either the right answer, or the maximum value for a
u8, if the right answer would overflow 8 bits.
For example, suppose our program controls a spacecraft, and its
rocket thrust setting is represented as a u8, meaning that
the allowable range is from 0 to 255.
If we’re already at 255—that is, at maximum thrust—and the user presses the “go faster” button again, that’s not an error, but we definitely don’t want the thrust setting to suddenly wrap around to zero! That would give the user a nasty case of whiplash.
The thrust shouldn’t change at all in this case, since it’s already
at maximum. To achieve that, we might use saturating_add to
clamp the output to 255 without wrapping.
The strict_add method
Sometimes we want to say that a particular sum should never overflow in any circumstances, and if that does happen, things have gone so disastrously wrong that the safest course of action is to stop the program (that is, panic). As we’ve seen, that’s what Rust already does implicitly, in dev builds, if we’re using the regular arithmetic operators.
To specify this “panic on overflow” behaviour explicitly, though, we
have the strict_add
method:
let answer = val.strict_add(i);In other words, we’re saying that the design of the program should have ensured that overflow never happens, and the fact that it has indicates an unrecoverable bug.
In general, our Rust programs should never panic unless this is the
case, but they should always panic when it is the case.
strict_add is the spell we need to ensure that happens.
Choose your own arithmetic adventure
It’s important to be aware of Rust’s default responses to overflow with the traditional arithmetic operators, so let’s tabulate them:
| Profile | Overflow result | Equivalent to |
|---|---|---|
dev |
Panic | strict_add |
release |
Wrap | wrapping_add |
I prefer using explicit arithmetic methods instead: this avoids potential confusion, and makes it clear to readers what will happen if a calculation overflows. To me, this seems very much in the spirit of Rust.
To summarise:
Method on
T |
Returns | Overflow result | Makes sense when |
|---|---|---|---|
checked_add |
Option<T> |
None |
We only want the answer if it’s not wrapped |
overflowing_add |
(T, bool) |
(Wrapped answer, true) |
We treat the answer differently if it’s wrapped |
wrapping_add |
T |
Wrapped answer | Wrapping is explicitly okay |
saturating_add |
T |
Answer or T::MAX, whichever
is lower |
We want the answer clamped to
T::MAX |
strict_add |
T |
Panic | Wrapping indicates a bug |
If you want to make sure that you don’t accidentally use one of the forbidden operators—and we all have moments of weakness—there’s a Clippy lint to remind you:
#
It can sometimes take a bit of extra thought to eliminate unchecked arithmetic in your Rust programs (“What do I really want to happen here? Is it okay to wrap? Or should this be an error?”). It’s worthwhile, though, as unhandled overflow can lead to all sorts of unpleasant bugs, including security issues.
In conclusion, if you don’t want to get your bits chopped off—and it’s as painful as it sounds—consider using Rust’s explicit checked arithmetic methods instead of the operators of death.
You can read more valuable medical advice about protecting the health of your programs (and your users) in The Rust Spellbook. Side effects may include better code, a deeper understanding of Rust, and a compulsion to tell people all the interesting stuff you learned. Ask your doctor if the Rust Spellbook is right for you.



