Error

"expected &str, found String" — How to Fix

Fix the 'expected &str, found String' error by converting the String to a slice with .as_str() or passing a reference with &.

The mismatch that stops you cold

You write a function to parse a configuration line. It takes a &str. You read a file and get a String. You pass the string to the function. The compiler halts with E0308 (mismatched types). It feels like Rust is being pedantic about a type that looks identical to the one you have.

The compiler is not guessing. It is refusing to decide whether you want to lend the data or give it away. String and &str are fundamentally different concepts in Rust's type system. One owns the memory. The other is a view into memory owned by someone else. Confusing them leads to accidental copies, dangling pointers, or ownership transfers you didn't intend. Rust forces you to make the choice explicit.

View versus container

Think of String as a potted plant you bought at a nursery. You own the pot, the soil, and the plant. You can move it to a new shelf, give it to a friend, or throw it away. You are responsible for its lifecycle.

&str is a photograph of that plant. The photo shows you the plant, but you don't own the plant. You can hand the photo to anyone. They can look at it. They cannot water the plant through the photo. If you throw away the real plant, the photo becomes useless. The photo must never outlive the plant.

In Rust, String is the potted plant. It allocates a buffer on the heap and owns it. &str is the photograph. It is a "fat pointer" containing a memory address and a length. It points into a buffer owned by a String, a string literal, or another source. It borrows the data. It cannot modify the buffer, and it cannot extend the buffer.

This distinction is structural. The compiler treats them as different types because they carry different guarantees. A function that takes &str promises not to take ownership. It promises the data will still be there when it returns. If Rust allowed you to pass a String directly, it would have to decide whether to borrow the string or move it. Moving the string would leave the caller with nothing, which is often a bug. Borrowing is safer, but Rust prefers explicit borrows so you can see the lifetime relationships in your code.

Memory layout: why they differ

The difference goes deeper than semantics. The in-memory representation of these types is different.

A String is a struct with three fields: a pointer to the heap buffer, the current length, and the capacity. Capacity is the amount of allocated space, which may be larger than the length to allow for efficient growth. When you push characters to a String, it uses the spare capacity. If it runs out, it allocates a new, larger buffer and copies the data.

A &str is a slice type with only two fields: a pointer and a length. It has no capacity. You cannot grow a &str. It is a fixed view. This layout difference is why they are distinct types. A String can mutate its capacity. A &str cannot. If you pass a String where a &str is expected, the compiler needs to know whether to extract the pointer and length (creating a view) or to hand over the whole struct (transferring ownership).

fn analyze(text: &str) {
    // text is a view. It has ptr and len.
    // It cannot grow. It cannot be mutated.
    println!("Length: {}", text.len());
}

fn main() {
    // owned is a container. It has ptr, len, and capacity.
    // It owns the heap buffer.
    let owned = String::from("Rust is safe");
    
    // Error: expected &str, found String.
    // The compiler sees a struct with 3 fields vs a slice with 2 fields.
    // analyze(owned);
    
    // Fix: create a view from the container.
    analyze(&owned);
}

The fix: borrow or convert

You have two standard ways to resolve this error. Both are correct. The choice depends on what you want to communicate.

The first approach is to add a reference. Prefix the variable with &. This creates a &String. Rust then applies a mechanism called deref coercion to convert &String into &str automatically. This is the most common fix. It signals that you are borrowing the string and the owner remains valid.

The second approach is to call .as_str(). This method returns a &str explicitly. It extracts the pointer and length from the String and hands you a slice. This is useful when you need to be explicit about the type, or when you are chaining methods and want to ensure the result is a slice.

fn greet(name: &str) {
    // name is borrowed. The caller keeps ownership.
    println!("Hello, {}", name);
}

fn main() {
    let owned_name = String::from("Alice");
    
    // Borrow the string. &owned_name is &String.
    // Deref coercion converts &String to &str.
    greet(&owned_name);
    
    // Explicitly extract a slice.
    // Returns &str directly.
    greet(owned_name.as_str());
    
    // owned_name is still valid. We didn't move it.
    println!("Name is still {}", owned_name);
}

Deref coercion: the hidden helper

The reason &owned_name works even though the type is &String is deref coercion. String implements the Deref trait with a target of str. When the compiler sees a reference to a type that implements Deref, it can automatically dereference it to get a reference to the target type.

This coercion happens transparently in function arguments and method calls. It allows &String to flow into parameters expecting &str. It also allows you to call &str methods on &String. For example, owned_name.len() works because String derefs to str, and str has the len method.

This coercion is a convenience, not a type equality. &String and &str are still different types. You cannot assign a &String to a variable typed as &str without coercion. The compiler handles the conversion at the call site.

Realistic scenario: processing config

In real code, you often read data from files or networks, which gives you Strings. You then pass that data to functions that process slices. The pattern is consistent: read into a String, borrow slices from it.

use std::fs;

/// Validates a config key. Takes a slice to avoid ownership.
fn is_valid_key(key: &str) -> bool {
    key.starts_with("cfg_") && key.len() > 4
}

fn main() {
    // Read the whole file into an owned String.
    let content = fs::read_to_string("config.txt").unwrap();
    
    // Iterate over lines. lines() yields &str slices.
    // Each slice borrows from content.
    for line in content.lines() {
        // line is &str. No conversion needed.
        if is_valid_key(line) {
            println!("Found key: {}", line);
        }
    }
    
    // Check a dynamic override stored as a String.
    let override_key = String::from("cfg_debug");
    
    // Borrow the override to pass to the function.
    if is_valid_key(&override_key) {
        println!("Override active");
    }
    
    // content and override_key are still owned by main.
    // They drop when main ends.
}

Pitfalls and compiler signals

The error E0308 appears whenever types don't match. In this context, it means you passed a String where a &str is expected. The compiler message will show "expected &str, found struct String".

A related pitfall is lifetime errors. If you try to return a &str derived from a local String, the compiler rejects it with E0515 (cannot return value referencing local variable). The slice must not outlive the owner.

fn bad_example() -> &str {
    let local = String::from("temporary");
    // Error: E0515. local is dropped at end of function.
    // The &str would point to freed memory.
    local.as_str()
}

Another pitfall is assuming &String and &str are interchangeable everywhere. They are not. You can pass &String to a function expecting &str due to coercion. You cannot pass &str to a function expecting &String because a slice doesn't have capacity information. The coercion is one-way.

Decision matrix

Use &variable when you have a String and the function takes &str. The borrow creates a &String, which Rust automatically coerces to &str via deref coercion. This is the idiomatic choice for most cases.

Use .as_str() when you need an explicit &str type, such as when assigning to a variable typed as &str, or when chaining methods where you want to emphasize the slice extraction.

Change the function parameter to String when the function must take ownership of the data, such as storing it in a struct, moving it into a thread, or consuming it to avoid a copy.

Reach for Cow<str> when writing a library function that accepts string-like data and you want to avoid forcing the caller to allocate a String if they only have a &str. Cow stands for "clone on write" and accepts either owned or borrowed strings.

Prefer &str in your function signatures unless you specifically need ownership. This convention makes your API flexible. Callers can pass string literals, slices, or borrow their Strings without allocation. It is the standard pattern in the Rust ecosystem.

Borrow the string. Keep the owner alive. The compiler protects you from dangling views by enforcing this rule at compile time.

Where to go next