How to Match a String Against String Literals in Rust (Without Tears)
If you’ve ever tried to do this in Rust: fn main() {
let stringthing = String::from("c");
match stringthing {
"a" => println!("0"),
"b" => println!("1"),
"c" => println!("2"),
}
} …and been greeted by a grumpy E0308: mismatched types, you’ve run head-first into one of Rust’s sharpest—and most helpful—edges: types matter. Let’s turn that compiler error into understanding and write idiomatic, readable code you’ll be proud to ship.
Summary Match on a borrowed &str, not on an owned String: fn main() {
let stringthing = String::from("c");
match stringthing.as_str() {
"a" => println!("0"),
"b" => println!("1"),
"c" => println!("2"),
_ => println!("something else!"),
}
} Why this works: as_str() borrows the String as &str, letting you match against string literals (which are also &'static str) without moving or allocating anything.
What Went Wrong (and Why)
1) "a" is &'static str, but you matched on String String is an owned, heap-allocated, growable string. A string literal like "a" is a &'static str: a borrowed string slice baked into your binary. In your first attempt, the match scrutinee is a String, so every pattern must also be a String. But your patterns were &str literals. Rust won’t implicitly convert between them, hence: expected std::string::String, found &'static str (E0308)
2) You can’t call functions in patterns Your second attempt tried: match stringthing {
String::from("a") => ...
// ...
} Patterns are not arbitrary expressions; they’re shapes to destructure values. String::from("a") is a function call, not a pattern. Hence: String::from does not name a tuple variant or tuple struct (E0164)
The Idiomatic Fix Borrow the String as a &str and match that: match stringthing.as_str() {
"a" => println!("0"),
"b" => println!("1"),
"c" => println!("2"),
_ => println!("something else!"),
}
- No move: You can still use stringthing after the match.
- No allocation: as_str() is zero-cost; it just lends you a view of the string.
- Perfect type fit: Now both sides are &str.
Why as_str() over as_ref()? You’ll often see this, and it also works: match stringthing.as_ref() {
"a" => println!("0"),
"b" => println!("1"),
"c" => println!("2"),
_ => println!("something else!"),
} .as_ref() uses the AsRef<str> trait, which is implemented for String. It’s flexible, but as_str() is more explicit and signals your intent clearly: “I want a &str view of this String.” The standard docs also showcase as_str() for this exact use case.
Make It Reusable A great pattern is to write functions that take &str. Then they happily accept either &str or String (via as_str()): fn code_for(s: &str) -> Option<u8> {
match s {
"a" => Some(0),
"b" => Some(1),
"c" => Some(2),
_ => None,
}
} fn main() {
let owned = String::from("b");
assert_eq!(code_for(&owned), Some(1)); // borrow &String -> &str
assert_eq!(code_for(owned.as_str()), Some(1)); // explicit
assert_eq!(code_for("c"), Some(2)); // literal
} Handy Variations You’ll Actually Use Multiple literals in one arm match stringthing.as_str() {
"y" | "yes" | "yeah" => println!("affirmative"),
"n" | "no" => println!("negative"),
_ => println!("huh?"),
} Match guards for conditions match stringthing.as_str() {
s if s.starts_with("cmd:") => println!("command: {}", &s[4..]),
_ => println!("not a command"),
} matches! for quick checks if matches!(stringthing.as_str(), "dev" | "staging" | "prod") {
println!("Known environment");
} Option<String>? Use .as_deref() When you have a Option<String>, .as_deref() converts it to Option<&str>: let maybe = Some(String::from("a")); match maybe.as_deref() {
Some("a") => println!("zero"),
Some(_) => println!("other"),
None => println!("missing"),
} Borrowing without as_str() (for completeness) These compile, but are less clear than as_str(): match &stringthing[..] { /* ... */ } // slice the whole string as &str // or match stringthing.as_ref() { /* ... */ } Common Pitfalls (and how to dodge them)
- Don’t allocate just to compare. Writing if stringthing == "a".to_string() allocates on every comparison. Prefer borrowing: if stringthing == "a" (which uses PartialEq<&str> for String) or match on as_str().
- Don’t move your String unless you mean to. match stringthing { ... } consumes it. If you still need it later, match on a borrowed view: match stringthing.as_str() { ... }.
- Don’t forget the catch-all. Exhaustive matches are required. If you don’t enumerate everything, add _ => ....
Error Messages, Decoded
- E0308: mismatched types You matched a String against &str patterns. Borrow or convert to &str.
- E0164: does not name a tuple variant or struct You tried to call a function (like String::from("a")) in a pattern. Patterns are shapes, not expressions.
Bonus: When a match Isn’t the Right Tool If you’re just mapping a handful of strings to outputs and that set changes a lot, a lookup table might be cleaner: use std::collections::HashMap; let mut map = HashMap::new(); map.insert("a", 0); map.insert("b", 1); map.insert("c", 2); if let Some(&n) = map.get(stringthing.as_str()) {
println!("{n}");
} else {
println!("something else!");
} This shines when the mapping is data, not code.
The Takeaway
- Match on &str, not on String.
- Use .as_str() (or .as_deref() for Option<String>) to get a borrowed view.
- Keep your code allocation-free, non-moving, and readable.
With those patterns in your toolbox, that once-cryptic error message becomes a friendly nudge toward idiomatic Rust. Happy matching!