Rust String Concatenation: A Friendly, No-Nonsense Guide (with Optimal Patterns)
Rust gives you two main string flavors: &str (a borrowed string slice) and String (an owned, heap-allocated, growable string). Concatenation is really just about where the bytes live and who owns them. Below is an engaging, practical tour through the most common combinations — plus a simple decision chart so you can pick the optimal approach for your use case.
Quick Primer: &str vs String
- &str — a view into some UTF-8 bytes; not growable; no allocation on its own.
- String — an owned, mutable buffer on the heap; growable; can allocate and re-allocate.
Rule of thumb: Whenever you combine pieces into a new result, someone must own the resulting bytes — i.e., you’ll end up with a String.
The Combinations You Asked About
1) &str + &str
You can’t use + directly (there’s no Add for &str). Create a new String via format!, concat, or join:
fn main() {
let a: &str = "hello ";
let b: &str = "world";
// Readable and flexible
let s1 = format!("{a}{b}");
// Works with slices/arrays of &str
let s2 = [a, b].concat();
let s3 = [a, b].join("");
println!("{s1} | {s2} | {s3}");
}
Bonus: Adjacent string literals concatenate at compile time with no allocation: let s = "hello " "world"; // "hello world"
2) String + &str
This is the classic, efficient “grow the left string” pattern:
fn main() {
let mut owned: String = "hello ".to_owned();
let borrowed: &str = "world";
owned.push_str(borrowed); // in-place append; may reuse allocation
println!("{owned}");
}
You can also use +, but note it consumes the left side:
fn main() {
let left: String = "hello ".to_owned();
let right: &str = "world";
let combined = left + right; // left is moved and no longer accessible
println!("{combined}");
}
3) String + String
Right-hand side must be borrowed as &str (the Add impl is String + &str):
fn main() {
let mut a: String = "hello ".to_owned();
let b: String = "world".to_owned();
// In-place, keeps `b` untouched
a.push_str(&b);
println!("{a} / still have b: {b}");
// Or consume left with `+` (borrow right)
let a2: String = "hello ".into();
let b2: String = "world".into();
let combined = a2 + &b2; // a2 moved, b2 remains
println!("{combined} / still have b2: {b2}");
}
When You Want a New String (leave inputs untouched)
format! is the simplest, most readable choice:
fn main() {
let a: &str = "hello ";
let b: &str = "world";
let together = format!("{a}{b}");
println!("{together}");
}
It also works with String (the compiler borrows them as &str inside format!):
fn main() {
let a: String = "hello ".into();
let b: String = "world".into();
let together = format!("{a}{b}");
println!("{together}");
}
You can clone to keep originals and use +, but it’s usually noisier and can be wasteful:
fn main() {
let a: String = "hello ".into();
let b: &str = "world";
let together = a.clone() + b; // a is cloned (extra allocation)
println!("{together}");
}
Beyond the Basics: Many Pieces, Loops, and Performance If you’re building a string incrementally (especially inside a loop), prefer a single String buffer and push into it:
fn main() {
let parts = ["user:", "alice", " id:", "42"];
let mut out = String::new();
// Optional but great for perf: estimate and reserve once
out.reserve(parts.iter().map(|s| s.len()).sum());
for p in parts {
out.push_str(p);
}
println!("{out}");
}
If you already have a collection of &str, join is concise and efficient:
fn main() {
let parts = vec!["foo", "bar", "baz"];
let s = parts.join(","); // "foo,bar,baz"
println!("{s}");
}
When you’re pushing single characters, use push:
out.push(' ');
out.push('🚀');
If you need to avoid allocations unless necessary, return a Cow<'a, str>:
use std::borrow::Cow;
fn maybe_title_case<'a>(s: &'a str, do_it: bool) -> Cow<'a, str> {
if do_it {
Cow::Owned(s.to_uppercase()) // allocates only in this branch
} else {
Cow::Borrowed(s) // zero allocation
}
}
If you only need to print and not keep a combined string, you can avoid allocation entirely: println!("{}{}", a, b); // formats directly to stdout
Optimal Choices (Cheat Sheet)
- Two to a few parts (clarity first): Use format!("{a}{b}{c}"). It’s clean, handles any mix of &str/String, and leaves inputs untouched.
- Build incrementally in a loop: Use a single String with with_capacity/reserve and push_str/push. This minimizes reallocations and copies.
- Combine a list of &str: Use parts.join("") (or parts.join(",") etc.). For a small fixed slice, [a, b, c].concat() works too.
- Prefer not to lose the left value: Don’t use + unless you want to move the left String. Instead, push_str on a mutable String, or use format!.
- Don’t clone just to concatenate: If you catch yourself writing a.clone() + ..., consider format! or push_str instead.
Common Pitfalls
- String + String without &: let s = a + b; fails because Add expects &str on the right. Use a + &b or a.push_str(&b).
- Repeated s = s + piece in a loop: This can reallocate every time. Use push_str on a pre-reserved String.
- Forgetting UTF-8: String is UTF-8. Concatenation is byte copy; you don’t need to worry about breaking characters unless you slice on byte boundaries improperly. Using the APIs above is safe.
Copy-Paste Examples You Can Run
Append borrowed to owned (reuses allocation)
fn main() {
let mut s: String = "hello ".to_owned();
let t: &str = "world";
s.push_str(t);
println!("{s}");
}
Keep both inputs, get a new String
fn main() {
let a: String = "hello ".into();
let b: &str = "world";
let c = format!("{a}{b}");
println!("{c}");
}
Consume left, borrow right (+)
fn main() {
let a: String = "hello ".into();
let b: String = "world".into();
let c = a + &b;
println!("{c} | still have b: {b}");
}
Many parts efficiently (reserve + push)
fn main() {
let parts = ["hello", " ", "world", "!"];
let total: usize = parts.iter().map(|s| s.len()).sum();
let mut out = String::with_capacity(total);
for p in parts {
out.push_str(p);
}
println!("{out}");
}
Summary Decision Chart
- Just print? → println!("{}{}", a, b); (no allocation)
- Return a new string from a few parts? → format!()
- Grow one string repeatedly? → String::with_capacity + push_str/push
- Have a list of &str? → .join("") or .concat()
- Want +? → Remember: moves left String, borrows right &str (a + &b)
Read the full article here: https://medium.com/@trivajay259/rust-string-concatenation-a-friendly-no-nonsense-guide-with-optimal-patterns-ad157b770c81