Question

How to Join a Vec<String> into One String in Rust

ruststring

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.

Take Quiz

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_list is a Vec<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:

  1. Rust creates a vector named items.
  2. The vector contains three String values:
    • "Foo"
    • "Bar"
    • "Baz"
  3. items.join("-") is called.
  4. Rust starts building a new String for the result.
  5. It adds the first item: "Foo"
  6. It sees another item exists, so it adds the separator: "-"
  7. It adds the next item: "Bar"
  8. It sees another item exists, so it adds another "-"
  9. It adds the final item: "Baz"
  10. The final string becomes "Foo-Bar-Baz"
  11. 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.

Take Quiz

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, join is 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

ConceptWhat it doesSeparator supportTypical use
join()Combines string elements into one StringYesHuman-readable text, CSV-style lines, paths
concat()Combines string elements into one StringNoSimple direct concatenation
format!()Builds a formatted string from valuesManualCustom sentence or message formatting
Manual loop with push_str()Appends text piece by pieceYes, but manualFine-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.

Take Quiz

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 new String
  • 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 separator
  • concat() does not
Take Quiz

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

  1. Create a function that takes a Vec<String>.
  2. Return all tags joined with ", ".
  3. If the vector is empty, return "No tags".
  4. In main, test the function with both a non-empty list and an empty list.
Take Quiz

Keep learning

Related questions

View more