Question
I want to combine the elements of a Vec<String> into a single String in Rust, similar to a join operation in other languages.
For example:
let string_list = vec!["Foo".to_string(), "Bar".to_string()];
let joined = /* how do I join these with "-"? */;
assert_eq!("Foo-Bar", joined);
What is the Rust way to join a vector of strings with a separator such as "-"?
A related question is how this compares to idiomatic string output when printing iterator items separated by spaces.
Short Answer
By the end of this page, you will understand how to join multiple strings into one String in Rust, especially from a Vec<String> or string slices. You will learn the join() method, what types it works on, how separators are used, and common patterns developers use in real Rust code.
Concept
In Rust, joining strings means taking multiple string values and combining them into one larger String, with an optional separator placed between each item.
The most common tool for this is the join() method on slices of strings. In practice, this is often used with:
Vec<String>Vec<&str>- slices like
&[String] - collections that can be viewed as string slices
For example, if you have these values:
"Foo""Bar""Baz"
and join them with "-", the result is:
Foo-Bar-Baz
This matters because combining text is very common in real programs:
- building file paths or labels
- generating CSV-like output
- constructing log messages
- formatting command-line output
- turning a list of values into a readable string
In Rust, string handling is explicit and type-safe. That means it is important to know whether you are working with:
String— owned, growable string data&str— borrowed string slice
The good news is that join() is ergonomic for common cases, and Rust provides trait implementations that make joining Vec<String> straightforward.
Mental Model
Think of join() like connecting train cars with a coupling placed between each car.
- Each string is a train car.
- The separator is the coupling, such as
"-"or", ". - The final result is one complete train.
If your list is:
["Foo", "Bar", "Baz"]
and your separator is "-", then join() builds:
Foo-Bar-Baz
Important detail: the separator goes between items, not before the first item and not after the last item.
Syntax and Examples
The basic Rust syntax is:
let joined = vec.join(separator);
Example with Vec<String>
fn main() {
let string_list = vec!["Foo".to_string(), "Bar".to_string()];
let joined = string_list.join("-");
assert_eq!("Foo-Bar", joined);
}
Why this works
string_listis aVec<String>join("-")combines all elements"-"is inserted between each element- the result is a new
String
Example with Vec<&str>
fn main() {
let words = vec!["red", "green", "blue"];
let result = words.join(", ");
assert_eq!("red, green, blue", result);
}
Joining without a separator
fn main() {
let parts = vec!["a", "b", "c"];
let result = parts.join("");
assert_eq!("abc", result);
}
Joining lines
fn main() {
let lines = vec!["first", "second", "third"];
let text = lines.join("\n");
println!("{}", text);
}
This is useful when building multi-line output.
Step by Step Execution
Consider this example:
fn main() {
let items = vec!["Foo".to_string(), "Bar".to_string(), "Baz".to_string()];
let result = items.join("-");
println!("{}", result);
}
Here is what happens step by step:
- Rust creates a vector named
items. - The vector contains three
Stringvalues:"Foo""Bar""Baz"
items.join("-")is called.- Rust starts building a new
Stringfor the result. - It adds the first item:
"Foo" - It sees another item exists, so it adds the separator:
"-" - It adds the next item:
"Bar" - It sees another item exists, so it adds another
"-" - It adds the final item:
"Baz" - The final string becomes
"Foo-Bar-Baz" println!prints that final string.
So the output is:
Foo-Bar-Baz
Notice that there is no trailing "-" at the end. join() handles that automatically.
Real World Use Cases
Joining strings appears in many common Rust tasks.
Building command output
let args = vec!["cargo", "build", "--release"];
let command = args.join(" ");
// "cargo build --release"
Creating CSV-style text
let fields = vec!["Alice", "24", "Engineer"];
let line = fields.join(",");
// "Alice,24,Engineer"
Formatting tags or labels
let tags = vec!["rust", "strings", "beginner"];
let display = tags.join(" | ");
// "rust | strings | beginner"
Constructing paths or keys
let parts = vec!["users", "42", "profile"];
let key = parts.join(":");
// "users:42:profile"
Multi-line logs or reports
let messages = vec!["Start", "Processing", "Done"];
let report = messages.join("\n");
These examples show why joining is a small but essential string-processing skill.
Real Codebase Usage
In real Rust codebases, developers often use join() after transforming data into text.
Pattern: map values to strings, then join
fn main() {
let numbers = vec![10, 20, 30];
let output = numbers
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ");
assert_eq!("10, 20, 30", output);
}
This pattern is common when original values are not strings.
Pattern: validation before joining
fn join_names(names: Vec<String>) -> String {
if names.is_empty() {
return "No names".to_string();
}
names.join(", ")
}
This uses a guard clause to handle an edge case first.
Pattern: formatting for logs or errors
fn main() {
let missing = ["email", "password"];
let message = format!("Missing fields: {}", missing.join(", "));
println!("{}", message);
}
Pattern: joining borrowed strings
Developers often avoid unnecessary allocation before the final join when possible.
fn main() {
let pieces: Vec<&str> = vec!["api", "v1", "users"];
let route = pieces.join("/");
assert_eq!("api/v1/users", route);
}
This is efficient and readable.
Common Mistakes
1. Trying to call join as a free function
Broken idea:
let joined = something::join(string_list, "-");
Why it is wrong:
- In Rust,
joinis usually called as a method on the collection or slice.
Correct version:
let joined = string_list.join("-");
2. Forgetting that join() returns a new String
let parts = vec!["a".to_string(), "b".to_string()];
let result = parts.join("-");
result is a new owned String. It does not modify each element in place.
3. Expecting join() to work directly on any iterator
Broken example:
let result = (1..=3).map(|n| n.to_string()).join(", ");
This usually fails because join() is not a method on standard iterators directly.
Common fix:
let result = (1..=3)
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ");
4. Confusing String and &str
Beginners sometimes think they must convert everything manually. In many common cases, Rust already supports joining string-like values conveniently.
let a = vec!["x", "y", "z"];
let b = vec!["x".to_string(), "y".to_string(), "z".to_string()];
assert_eq!(a.join("-"), "x-y-z");
assert_eq!(b.join("-"), "x-y-z");
5. Manually adding separators in a loop
Broken or awkward version:
let items = vec!["Foo", "Bar", "Baz"];
let mut result = String::new();
for item in items {
result.push_str(item);
result.push('-');
}
assert_eq!("Foo-Bar-Baz", result); // fails: result is "Foo-Bar-Baz-"
Better approach:
let items = vec!["Foo", "Bar", "Baz"];
let result = items.join("-");
assert_eq!("Foo-Bar-Baz", result);
6. Using concat() when you really need separators
let items = vec!["Foo", "Bar"];
assert_eq!("FooBar", items.concat());
concat() combines items with no separator. Use join() when separators matter.
Comparisons
| Concept | What it does | Separator support | Typical use |
|---|---|---|---|
join() | Combines string elements into one String | Yes | Human-readable text, CSV-style lines, paths |
concat() | Combines string elements into one String | No | Simple direct concatenation |
format!() | Builds a formatted string from values | Manual | Custom sentence or message formatting |
Manual loop with push_str() | Appends text piece by piece | Yes, but manual | Fine-grained control |
join() vs concat()
fn main() {
let items = vec!["a", "b", "c"];
assert_eq!("abc", items.concat());
assert_eq!("a-b-c", items.join("-"));
}
join() vs format!()
Use join() when you already have a list.
let parts = vec!["red", "green", "blue"];
let s = parts.join(", ");
Use format!() when building a custom string shape.
let name = "Alice";
let age = 30;
let s = format!("{} is {} years old", name, age);
join() vs manual loops
Manual loops are more flexible, but join() is simpler and less error-prone for standard separator-based joining.
Cheat Sheet
Quick syntax
let result = items.join(", ");
Works well with
Vec<String>Vec<&str>- slices of string-like values
Examples
let a = vec!["Foo".to_string(), "Bar".to_string()];
let b = a.join("-");
// "Foo-Bar"
let a = vec!["x", "y", "z"];
let b = a.join("");
// "xyz"
Use concat() when no separator is needed
let items = vec!["a", "b", "c"];
let s = items.concat();
// "abc"
Joining non-strings
Convert first, then join:
let nums = vec![1, 2, 3];
let s = nums.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ");
Important rules
join()returns a newString- the separator is placed between items
- no trailing separator is added
- if the collection is empty, the result is
"" - if the collection has one item, that item is returned unchanged as a
String
Common separator examples
", ""-""/""\n"""
FAQ
How do I join a Vec<String> in Rust?
Use the join() method:
let items = vec!["Foo".to_string(), "Bar".to_string()];
let result = items.join("-");
Can I join a Vec<&str> too?
Yes.
let items = vec!["Foo", "Bar"];
let result = items.join("-");
What does join() return in Rust?
It returns a new owned String.
Does join() add the separator at the end?
No. The separator is only inserted between items.
What happens if the vector is empty?
join() returns an empty string: "".
How do I join numbers in Rust?
Convert them to strings first:
let result = vec![1, 2, 3]
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ");
Is join() available on iterators directly?
Not in the standard iterator API. A common approach is to collect into a Vec first, then call join().
What is the difference between join() and concat()?
join()inserts a separatorconcat()does not
Mini Project
Description
Build a small Rust utility that turns a list of tags into a display string. This demonstrates how join() is used in real applications to format user-facing text from a collection of values.
Goal
Create a function that accepts a list of tags and returns a single comma-separated string.
Requirements
- Create a function that takes a
Vec<String>. - Return all tags joined with
", ". - If the vector is empty, return
"No tags". - In
main, test the function with both a non-empty list and an empty list.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.