When types don't match
You're building a price calculator. You pull a quantity from a database as an i32 and a price from a config file as a String. You write quantity + price expecting the code to work. The compiler rejects it with E0369: "binary operation + cannot be applied to type i32". You're not making a logic error. You're asking Rust to perform a conversion it refuses to do implicitly.
In JavaScript, "5" + 1 becomes "51". In Python, 5 + "1" raises a TypeError at runtime. Rust checks this at compile time and demands you state exactly what you want. There is no hidden coercion. If the types don't match the operator's requirements, the code does not compile.
The trait behind the operator
Every binary operator in Rust maps to a trait. The + operator uses the std::ops::Add trait. The == operator uses std::cmp::PartialEq. The - operator uses std::ops::Sub. When you write a + b, the compiler desugars this to a method call on the trait. Roughly, a + b becomes Add::add(a, b).
The compiler performs trait resolution. It looks at the type of a and searches for an implementation of Add where the right-hand side matches the type of b. If a is i32 and b is i32, the standard library provides impl Add<i32> for i32. The compiler finds it and generates the addition instruction. If b is &str, the compiler searches for impl Add<&str> for i32. It finds nothing. E0369 triggers.
Think of operators as specialized tools. The integer addition tool knows how to add bits. The string concatenation tool knows how to join characters. There is no universal tool that handles everything. If you hand the compiler an integer and a string, it doesn't know which tool to grab. It stops and asks you to clarify.
Minimal example: strings and numbers
This example shows the error and the fix. The goal is to add a numeric value parsed from a string to an integer.
fn main() {
let quantity = 5;
let price_str = "10";
// E0369: cannot add &str to i32.
// Rust refuses to guess that you want to parse the string first.
// let total = quantity + price_str;
// Fix: parse the string to a number before adding.
// The parse method returns a Result, so we handle the error case.
let price: i32 = price_str.parse().expect("Invalid price format");
let total = quantity + price;
println!("Total: {}", total);
}
The parse method converts the string to a number. You must specify the target type with a type annotation like let price: i32. Once both operands are i32, the Add trait implementation exists, and the code compiles.
How the compiler resolves operators
When you write an expression with a binary operator, the compiler follows a strict resolution path. First, it determines the types of both operands. If a type is unknown, it tries to infer it from context. If inference fails, you get a type mismatch error before E0369 even appears.
Once types are known, the compiler checks for the relevant trait. For +, it checks Add. The trait is generic over the right-hand side type. The signature looks like this:
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
The Rhs parameter allows a + b where a and b have different types. The Output associated type allows the result to be a third type. For example, i32 + i32 returns i32, but a hypothetical implementation could make i32 + u32 return u64. The compiler checks if the left operand's type implements Add for the right operand's type. If the implementation exists, it checks if the Output type matches the expected type in the surrounding context. If any step fails, E0369 appears.
Realistic scenario: generic arithmetic
Generic functions often trigger E0369 because the compiler doesn't know if a type parameter supports the operation. You write a function that adds two values, but you forget to constrain the type parameter.
/// Adds two values of the same type.
/// This version fails to compile because T might not support addition.
fn sum_broken<T>(a: T, b: T) -> T {
// E0369: binary operation + is not supported for T
a + b
}
The compiler rejects this because T could be anything. It might be a struct without an Add implementation. It might be a type where addition doesn't make sense. You must add a trait bound to tell the compiler that T supports addition.
use std::ops::Add;
/// Adds two values of the same type.
/// Requires T to implement Add and return T.
fn sum<T: Add<Output = T>>(a: T, b: T) -> T {
// The compiler now knows T supports + and returns T.
a + b
}
fn main() {
let x = sum(5, 10);
let y = sum(3.14, 2.71);
println!("{} {}", x, y);
}
The bound T: Add<Output = T> does two things. It requires T to implement Add. It also requires the Output associated type to be T. This ensures the function returns the same type it takes. Without Output = T, the compiler cannot guarantee the return type matches the function signature.
Convention aside: when writing generic arithmetic, always specify Output = T unless you have a reason to return a different type. It makes the intent clear and prevents subtle type errors downstream.
Custom types and the Output associated type
If you define a struct, it doesn't support operators by default. You get E0369 if you try to use + on your struct. You must implement the trait manually. This is how you give your types ergonomic syntax.
use std::ops::Add;
#[derive(Debug, Clone)]
struct Vector2 {
x: f64,
y: f64,
}
impl Add for Vector2 {
// The result of adding two Vector2s is a Vector2.
type Output = Vector2;
fn add(self, other: Vector2) -> Vector2 {
Vector2 {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
let v1 = Vector2 { x: 1.0, y: 2.0 };
let v2 = Vector2 { x: 3.0, y: 4.0 };
let v3 = v1 + v2;
println!("{:?}", v3);
}
The impl Add for Vector2 block provides the logic. The type Output = Vector2 line declares the return type. The add method performs the component-wise addition. Once implemented, v1 + v2 works exactly like built-in types.
You can also implement Add for different right-hand types. For example, you might want to add a scalar to a vector.
impl Add<f64> for Vector2 {
type Output = Vector2;
fn add(self, scalar: f64) -> Vector2 {
Vector2 {
x: self.x + scalar,
y: self.y + scalar,
}
}
}
Now vector + 5.0 compiles. The trait system allows flexible operator overloading while keeping type safety.
Common pitfalls and fixes
E0369 appears in several specific patterns. Recognizing these patterns speeds up debugging.
Signed and unsigned integers
Rust does not implicitly convert between signed and unsigned integers. Mixing them in an operation causes E0369.
fn main() {
let a: i32 = 5;
let b: u32 = 10;
// E0369: cannot add u32 to i32
// let c = a + b;
// Fix: cast one operand to match the other.
let c = a as u32 + b;
println!("{}", c);
}
Use as to cast explicitly. Be aware that casting a negative i32 to u32 wraps around to a large positive number. Verify the value fits before casting if correctness depends on it.
Comparing Option values
You cannot compare an Option<T> directly to a T. The types are different.
fn main() {
let opt = Some(5);
// E0369: cannot compare Option<i32> to i32
// if opt == 5 { }
// Fix: compare to Some(5) or use pattern matching.
if opt == Some(5) {
println!("Match");
}
}
Use Some(value) to wrap the concrete value, or use match to handle Some and None separately. Comparing Option to T hides the possibility of None, which defeats the purpose of the type.
References versus values
Sometimes you have a reference and a value. Auto-deref often handles this, but not always. If E0369 appears, check if one operand is a reference.
fn main() {
let a = 5;
let b = &10;
// This usually works due to auto-deref, but can fail in complex contexts.
// let c = a + b;
// Explicit dereference removes ambiguity.
let c = a + *b;
println!("{}", c);
}
Use * to dereference explicitly when the compiler complains. This makes the intent clear and resolves the type mismatch.
Missing trait bounds on structs
If you derive PartialEq on a struct, all fields must also implement PartialEq. If a field doesn't, the derive macro fails, and you might see E0369 when comparing instances.
// This fails to compile because Vec<T> does not implement PartialEq by default.
// #[derive(PartialEq)]
// struct Container {
// items: Vec<i32>,
// }
// Fix: implement PartialEq manually or use a type that supports it.
#[derive(PartialEq)]
struct Container {
items: Vec<i32>,
}
// Note: Vec<i32> actually does implement PartialEq in modern Rust.
// This example illustrates the principle for custom types or older versions.
Check that all fields support the required trait. If a field is a custom type, ensure it derives or implements the trait.
Decision matrix: choosing the right fix
Use as to cast between numeric types when you need to mix signed and unsigned integers, or when you are narrowing a type and have verified the value fits.
Use .parse() to convert strings to numbers when data comes from external sources like config files or user input.
Use trait bounds like T: Add<Output = T> when writing generic functions that perform arithmetic on type parameters.
Use impl std::ops::... to implement operators for your own structs when you want ergonomic syntax for custom types.
Use pattern matching to compare Option<T> or Result<T> against concrete values when you need to handle the wrapper logic explicitly.
Use explicit dereferencing with * when references and values cause ambiguity in operator resolution.
Rust doesn't hide conversions. You write the conversion, you own the risk. Pick the tool that matches the mismatch.