Jump to content

How to Match a String Against String Literals in Rust (Without Tears)

From JOHNWICK

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!