How to Replace Text in a String in Rust

Use the replace method on a String to generate a new string with the specified text substituted.

How to Replace Text in a String in Rust

You're processing a configuration file. Every instance of {{HOST}} needs to become 127.0.0.1. In Python or JavaScript, you call .replace() and the string updates. In Rust, the method exists, but the ownership model forces you to handle the result explicitly. You cannot mutate a String in place without paying for the allocation, and the compiler will not let you forget the new value.

The ownership model changes the result

Rust strings are immutable by default in the sense that String does not provide a method to modify its contents without taking ownership or a mutable reference. The replace method returns a brand new String. It does not modify the input.

Think of replace like a photocopier. You feed in the original document, the machine produces a copy with the text swapped, and hands you the copy. The original document sits untouched on the desk. If you want the variable to hold the new text, you must put the copy back in the variable.

This design prevents accidental data loss. If you pass a string to a function that replaces text, the caller still has the original data. The function cannot silently mutate shared state.

fn main() {
    // Create a heap-allocated string.
    let original = String::from("hello world");

    // replace returns a NEW String.
    // It borrows original, scans it, and allocates a fresh buffer for the result.
    let modified = original.replace("world", "Rust");

    // original is unchanged. Both strings exist simultaneously.
    println!("Original: {original}");
    println!("Modified: {modified}");
}

The original string survives the operation. If you want to update the variable, you must reassign it.

What happens under the hood

Calling replace triggers a full allocation. The method scans the input string byte by byte, looking for the pattern. When it finds a match, it copies the replacement text into a new buffer. When it finds non-matching text, it copies that text as-is. The result is a new String with capacity exactly matching the new length.

The cost is linear with the size of the input. Replacing one character in a megabyte string still requires allocating a new megabyte buffer and copying almost all the data. This is safe and predictable. It avoids the complexity of in-place resizing, which can invalidate pointers or require moving data around in memory.

replace is defined on str, not String. This matters. String implements Deref<Target=str>, which allows the compiler to automatically dereference the String to its underlying slice when you call methods. You can call replace on a String, a &String, a &str, or a string literal. The API is uniform.

Convention aside: String::replace and str::replace are the same function. The community treats str as the trait-like interface for string operations. When reading documentation, look at str methods first. They apply to owned strings too.

Unicode handling is automatic. Rust strings are UTF-8 encoded. replace operates on byte patterns but respects character boundaries. If your pattern matches a multi-byte sequence, the replacement happens atomically. You never get a corrupted string with half a character. The buffer resizes to accommodate the new bytes. This safety comes at a negligible cost. The implementation verifies UTF-8 validity during the scan, but the overhead is minimal compared to the allocation.

Trust the allocation. It keeps the original safe and the logic simple.

Real-world usage: Templates and chaining

A common pattern is replacing multiple placeholders. You can chain replace calls. Each call returns a new String, which becomes the input for the next call.

fn process_template(template: &str, host: &str, port: &str) -> String {
    // First pass: replace the host placeholder.
    // This allocates a new String.
    let step1 = template.replace("{{HOST}}", host);

    // Second pass: replace the port.
    // step1 is moved into this call.
    // The intermediate buffer is freed after this call returns.
    let final_config = step1.replace("{{PORT}}", port);

    final_config
}

fn main() {
    let tmpl = "Connect to {{HOST}} on port {{PORT}}";
    let result = process_template(tmpl, "127.0.0.1", "8080");
    println!("{result}");
}

Chaining works cleanly. The compiler moves the intermediate String into the next call, so you don't leak memory. Each step allocates a new buffer, copies the data, and drops the old buffer. For small strings or a few replacements, this is fast enough. The allocation overhead is tiny.

If you're processing a large file with dozens of replacements, the intermediate allocations add up. You allocate a new buffer for every pass. The total memory traffic grows proportional to the number of replacements times the file size. In performance-critical code, this can become a bottleneck.

Chain for simplicity. Optimize only when profiling demands it.

Pitfalls and gotchas

Developers coming from JavaScript often trip on the behavior of replace. In JavaScript, str.replace("a", "b") replaces only the first occurrence. In Rust, replace replaces all occurrences. There is no global flag. The method is always greedy.

If you need to limit the number of replacements, use replacen. It takes a count argument and stops after that many matches.

fn main() {
    let text = String::from("a-b-c-d");

    // replace all hyphens with underscores.
    let all = text.replace("-", "_");
    println!("{all}"); // a_b_c_d

    // replace only the first two hyphens.
    let limited = text.replacen("-", "_", 2);
    println!("{limited}"); // a_b_c-d
}

Another pitfall is expecting regex support. replace matches literal substrings. It does not understand wildcards, character classes, or anchors. Passing "\\d+" as the pattern will look for the literal characters backslash, d, and plus. It will not match numbers.

If you need pattern matching, reach for the regex crate. It provides Regex::replace, which accepts a pattern and returns a String. The API is similar, but the underlying engine is a finite automaton.

Convention aside: replacen is rarely used in idiomatic code. Most Rust code prefers replace for full substitution or regex for partial or complex substitution. replacen exists for edge cases where you want to avoid scanning the rest of the string after N matches.

In-place replacement is possible but requires manual work. String provides replace_range, which takes a range and a replacement slice. You must find the range yourself. This avoids allocating a new buffer, but you must handle the logic for multiple occurrences and shifting indices.

fn replace_first_in_place(s: &mut String, pattern: &str, replacement: &str) {
    // Find the first occurrence.
    if let Some(start) = s.find(pattern) {
        let end = start + pattern.len();

        // Replace the range in place.
        // The buffer may reallocate if the replacement is longer.
        s.replace_range(start..end, replacement);
    }
}

fn main() {
    let mut config = String::from("host=localhost");
    replace_first_in_place(&mut config, "localhost", "127.0.0.1");
    println!("{config}");
}

In-place mutation is faster but riskier. You manage indices and potential reallocations manually. Use it only when you've measured the cost.

Decision matrix

Use replace when you need to swap all literal substrings and the input is small enough that a full allocation is cheap.

Use replacen when you only want to change the first N occurrences and want to avoid scanning the rest of the string.

Use regex::Regex::replace when your pattern involves wildcards, character classes, or complex matching logic.

Use str::replace on a slice when you have a &str and want to avoid converting to String just to call the method.

Use replace_range when you're performing a single replacement on a large string and need to avoid allocation to meet strict performance constraints.

Pick the tool that matches your pattern complexity and performance constraints. Don't reach for regex for a literal swap.

Where to go next