Skip to content
rusthacks.dev
Generics

Rust Error E0369 Binary Operation Cannot Be Applied

E0369 means an operator like + or == was used on a type that does not implement the matching trait. Fix it with a derive, a trait bound on the generic, or an explicit impl.

9 min read
Summarize in AI
Flat illustration for the post “Rust Error E0369 Binary Operation Cannot Be Applied”, composed around generics.

E0369 appears when you apply a binary operator to a type that has no implementation of the trait behind it. a + b is sugar for Add::add(a, b), a == b for PartialEq::eq(&a, &b), so a type without that trait cannot use the symbol. There are four places you meet this: your own struct, a generic function, a reference, and a foreign type. All snippets compiled on Rust 1.99, edition 2024.

Your own struct has no operators yet

rust
struct Money {
    cents: i64,
}

fn main() {
    let a = Money { cents: 250 };
    let b = Money { cents: 175 };
    let total = a + b;
    println!("{}", total.cents);
}
Text
error[E0369]: cannot add `Money` to `Money`
 --> src/main.rs:8:19
  |
8 |     let total = a + b;
  |                 - ^ - Money
  |                 |
  |                 Money
  |
note: an implementation of `Add` might be missing for `Money`
 --> src/main.rs:1:1
  |
1 | struct Money {
  | ^^^^^^^^^^^^ must implement `Add`
note: the trait `Add` must be implemented
 --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/ops/arith.rs:76:0

Rust never invents an operator for a struct, even when every field would support it, because + on money might mean currency checks, saturation or rounding, and that is the author's decision. Implement std::ops::Add yourself:

rust
use std::ops::Add;

#[derive(Debug, Clone, Copy, PartialEq)]
struct Money {
    cents: i64,
}

impl Add for Money {
    type Output = Money;

    fn add(self, rhs: Money) -> Money {
        Money { cents: self.cents + rhs.cents }
    }
}

fn main() {
    let a = Money { cents: 250 };
    let b = Money { cents: 175 };
    println!("{:?}", a + b);
    println!("{}", a == Money { cents: 250 });
}
Text
Money { cents: 425 }
true

type Output is part of the contract: adding two Money values yields whatever you declare. It is why Instant - Instant can return a Duration, and it is the line most people forget to write at first. The == works here because of the PartialEq derive, which is the only operator trait that derives; arithmetic always needs a handwritten impl.

Comparison operators need PartialEq or PartialOrd

rust
struct Version {
    major: u32,
    minor: u32,
}

fn main() {
    let a = Version { major: 1, minor: 4 };
    let b = Version { major: 1, minor: 9 };
    println!("{}", a < b);
}
Text
error[E0369]: binary operation `<` cannot be applied to type `Version`
 --> src/main.rs:9:22
  |
9 |     println!("{}", a < b);
  |                    - ^ - Version
  |                    |
  |                    Version
  |
note: an implementation of `PartialOrd` might be missing for `Version`
 --> src/main.rs:1:1
  |
1 | struct Version {
  | ^^^^^^^^^^^^^^ must implement `PartialOrd`
help: consider annotating `Version` with `#[derive(PartialEq, PartialOrd)]`
  |
1 + #[derive(PartialEq, PartialOrd)]
2 | struct Version {
  |

A derive fixes it, and derives compare fields in declaration order, which is exactly what you want for a version:

rust
#[derive(PartialEq, PartialOrd)]
struct Version {
    major: u32,
    minor: u32,
}

fn main() {
    let a = Version { major: 1, minor: 4 };
    let b = Version { major: 1, minor: 9 };
    println!("{}", a < b);
}
Text
true

Field order matters. Swap the two fields and 1.4 < 1.9 becomes a comparison of minor first. The relationship among the four comparison traits is in PartialEq, Eq, PartialOrd and Ord.

A generic function knows nothing about T

This is the most frequent real-world source. Inside fn sum<T> the compiler has no idea what T can do, so even + is rejected:

rust
fn add_all<T>(a: T, b: T, c: T) -> T {
    a + b + c
}

fn main() {
    println!("{}", add_all(1, 2, 3));
}
Text
error[E0369]: cannot add `T` to `T`
 --> src/main.rs:2:7
  |
2 |     a + b + c
  |     - ^ - T
  |     |
  |     T
  |
help: consider restricting type parameter `T` with trait `Add`
  |
1 | fn add_all<T: std::ops::Add>(a: T, b: T, c: T) -> T {
  |             +++++++++++++++

The error is deliberately exact: it names the missing bound. Apply it, and note the Output = T, which says that adding two T yields another T. Without it, a + b has an unknown type, and adding a third value would fail again:

rust
use std::ops::Add;

fn add_all<T: Add<Output = T>>(a: T, b: T, c: T) -> T {
    a + b + c
}

fn main() {
    println!("{}", add_all(1, 2, 3));
    println!("{}", add_all(0.5, 0.25, 0.125));
    println!("{}", add_all(String::from("a"), "b".into(), "c".into()));
}
Text
error[E0277]: cannot add `String` to `String`
  --> src/main.rs:10:20
   |
10 |     println!("{}", add_all(String::from("a"), "b".into(), "c".into()));
   |                    ^^^^^^^ ----------------- `String` doesn't satisfy the trait bound
   |                    |
   |                    no implementation for `String + String`
   |
help: the trait `Add<String>` is not implemented for `String`
      but trait `Add<&str>` is implemented for it
  --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/alloc/src/string.rs:2808:0
   = help: for that trait implementation, expected `&str`, found `String`
note: required by a bound in `add_all`
  --> src/main.rs:3:15
   |
 3 | fn add_all<T: Add<Output = T>>(a: T, b: T, c: T) -> T {
   |               ^^^^^^^^^^^^^^^ required by this bound in `add_all`

The last call fails to compile, and it is worth seeing why: String + String does not exist, because the standard library implements Add<&str> for String instead. The bound Add<Output = T> means "T plus T", which a String cannot satisfy. That is a real limitation, not a compiler quirk, and the next section shows the shape that handles it.

Operators on references need their own bound

For concrete integers, &a + &b works, because the standard library implements Add for references to numbers. In generic code, T: Add covers values only. If you want to add without taking ownership, ask for the reference implementation with a higher-ranked bound:

rust
use std::ops::Add;

fn total_of<T>(items: &[T], zero: T) -> T
where
    T: Clone,
    for<'a> &'a T: Add<&'a T, Output = T>,
{
    let mut acc = zero;
    for x in items {
        acc = &acc + x;
    }
    acc
}

fn main() {
    println!("{}", total_of(&[1, 2, 3, 4], 0));
    println!("{}", total_of(&[1.5, 2.5], 0.0));
}
Text
10
4

That signature is heavy. If your function only needs numbers, T: Copy + Add<Output = T> is far easier to read, and the call site passes copies. Reach for the for<'a> form only when T is expensive to clone, such as a big-integer type.

Foreign types: the orphan rule gets in the way

You cannot implement Add (a foreign trait) for Vec<i32> (a foreign type), because of the orphan rule. The fix is a newtype that you own:

rust
use std::ops::Add;

#[derive(Debug)]
struct Vector(Vec<i32>);

impl Add for Vector {
    type Output = Vector;

    fn add(self, rhs: Vector) -> Vector {
        Vector(self.0.iter().zip(rhs.0.iter()).map(|(a, b)| a + b).collect())
    }
}

fn main() {
    let v = Vector(vec![1, 2, 3]) + Vector(vec![10, 20, 30]);
    println!("{v:?}");
}
Text
Vector([11, 22, 33])

The orphan rule is covered in full in the orphan rule, and the pattern in the newtype pattern. If you are adding two vectors in anger, a crate like nalgebra already provides the operators and the right semantics for mismatched lengths, where this example silently truncates.

Mixed types and the Rhs parameter

Add is really Add<Rhs = Self>. To allow money + cents you implement the trait a second time with a different right-hand type, and the compiler picks by operand type:

rust
use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Money(i64);

impl Add for Money {
    type Output = Money;
    fn add(self, rhs: Money) -> Money {
        Money(self.0 + rhs.0)
    }
}

impl Add<i64> for Money {
    type Output = Money;
    fn add(self, rhs: i64) -> Money {
        Money(self.0 + rhs)
    }
}

fn main() {
    println!("{:?}", Money(100) + Money(50));
    println!("{:?}", Money(100) + 25);
}
Text
Money(150)
Money(125)

It is not symmetric: 25 + Money(100) still fails, because that would be impl Add<Money> for i64, and the orphan rule forbids it since i64 is foreign and the trait is foreign. Document the one direction you support.

What the operator really calls

An operator is a method call written in a shorter form, and writing the long form once makes every E0369 easier to read. The expression a + b becomes Add::add(a, b), and the type of a decides which implementation runs. The trait takes its arguments by value, which is why the compiler moves non-Copy operands into the call.

rust
use std::ops::Add;

#[derive(Debug)]
struct Tag(String);

impl Add for Tag {
    type Output = Tag;
    fn add(self, rhs: Tag) -> Tag {
        Tag(format!("{}-{}", self.0, rhs.0))
    }
}

fn main() {
    let a = Tag("net".into());
    let b = Tag("io".into());
    let joined = a + b;
    println!("{joined:?}");
    println!("{a:?}");
}
Text
error[E0382]: borrow of moved value: `a`
  --> src/main.rs:18:16
   |
14 |     let a = Tag("net".into());
   |         - move occurs because `a` has type `Tag`, which does not implement the `Copy` trait
15 |     let b = Tag("io".into());
16 |     let joined = a + b;
   |                  ----- `a` moved due to usage in operator
17 |     println!("{joined:?}");
18 |     println!("{a:?}");
   |                ^ value borrowed here after move
   |
note: if `Tag` implemented `Clone`, you could clone the value
  --> src/main.rs:4:1
   |
 4 | struct Tag(String);
   | ^^^^^^^^^^ consider implementing `Clone` for this type
...
16 |     let joined = a + b;
   |                  - you could clone this value
note: calling this operator moves the left-hand side
  --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/ops/arith.rs:91:11

This is the same ownership rule from E0382 arriving through an operator. Adding two Tag values consumed both, because add takes self by value. Types that are cheap to copy derive Clone and Copy so this never bites, and types that are not provide impl Add for &Tag as well.

The sibling errors: += and unary minus

Compound assignment and unary operators have their own traits, and each has its own code, so E0369 is only the binary family. += uses AddAssign (E0368), and -x uses Neg (E0600). Implementing Add does not give you +=, because the assigning form can often avoid allocating a new value.

rust
use std::ops::{AddAssign, Neg};

#[derive(Debug, Clone, Copy)]
struct Money(i64);

impl AddAssign for Money {
    fn add_assign(&mut self, rhs: Money) {
        self.0 += rhs.0;
    }
}

impl Neg for Money {
    type Output = Money;
    fn neg(self) -> Money {
        Money(-self.0)
    }
}

fn main() {
    let mut balance = Money(500);
    balance += Money(250);
    println!("{balance:?}");
    println!("{:?}", -balance);
}
Text
Money(750)
Money(-750)

A rule of thumb that saves time: when you add an operator to a type, ask whether callers will also want the assigning form and the reference form, and implement them in the same change. A type where a + b works but a += b does not feels unfinished to the person using it.

Reading the error when it is long

E0369 messages can run to fifteen lines because the compiler tries to help. Three lines carry the information. The first names the operator and the type on the left. The one with the caret under the operator shows which side was inspected, since Rust picks the implementation from the type of the left operand. The last help or note shows the bound or derive that would fix it. If you cannot see a derive suggestion, the trait is not derivable and you need an impl, which is the case for every arithmetic operator.

One more practical point: a call to a function with a missing operator bound reports the error inside the function, not at the call. A generic helper that compiles for the first caller and breaks at the second means the bound is missing from the signature, and that error will only surface when somebody passes a type that happens to lack the operator. Writing the bound on T up front turns that late surprise into a signature the caller can read.

When an operator is the wrong design

Operators are for types where the operation is obvious and total. For fallible arithmetic, such as money that must not overflow or must have matching currencies, an Add that panics or silently wraps is a hazard. Provide checked_add returning Option or Result, the way the integer types do, and keep + for the unchecked case you are willing to document. The error family this belongs to is in E0277.

Taggede0369
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.