How to use Vec in Rust

Vec<T> is Rust's default growable list. Learn how it allocates, when to use Vec::new vs vec! vs with_capacity, common pitfalls, and when to pick another collection.

The list that grows

You're writing your first real Rust program and you need a list. In Python you'd write nums = [] and start appending. In JavaScript, const nums = []. Rust has arrays, but they're fixed-size: once you write [i32; 5], you've committed to exactly five slots, forever. Real programs rarely know how many things they'll have ahead of time. You read a file line by line, you parse user input, you accumulate results from a loop. You need a list that can grow.

That list is Vec<T>. The T is the type of thing you're storing. A Vec<i32> holds 32-bit integers. A Vec<String> holds owned strings. A Vec<User> holds whatever you defined User to be. The vector itself lives on the stack as a small struct (a pointer, a length, and a capacity), but the actual elements live on the heap, in a contiguous block. When you push more things than the current block can hold, Vec quietly allocates a bigger block, copies the old elements over, and frees the old one. From your side, it just keeps working.

Two ways to make one

There are two common ways to create a vector and they're both worth knowing.

fn main() {
    // Empty vector. The type annotation is required because there's nothing
    // inside that the compiler can use to infer the element type.
    let empty: Vec<i32> = Vec::new();

    // Pre-populated vector. The vec! macro is shorthand for "make a Vec
    // and put these elements in it." The compiler infers Vec<i32> from
    // the integer literals.
    let nums = vec![1, 2, 3];

    // You can also ask for N copies of the same value. Useful when
    // initialising a buffer.
    let zeros = vec![0u8; 1024];   // 1024 zero bytes

    println!("{} {} {:?}", empty.len(), nums.len(), &zeros[..4]);
}

Vec::new() doesn't allocate. It hands you a vector with capacity zero. The first push triggers an allocation. vec![1, 2, 3] allocates room for three elements right away. If you know roughly how many things you'll add, you can also write Vec::with_capacity(100) to pre-reserve space and skip the regrow-and-copy dance for the first hundred pushes. That matters in tight loops; it doesn't matter at all in casual code.

Adding, reading, removing

The three operations you'll use 90% of the time:

fn main() {
    // `mut` is required because we're going to modify the vector in place.
    let mut names: Vec<String> = Vec::new();

    // push appends one element to the end. O(1) amortised because of
    // the doubling-capacity trick.
    names.push(String::from("alice"));
    names.push(String::from("bob"));

    // Index access. This panics at runtime if the index is out of bounds.
    // Use `names.get(0)` if you want a safe Option<&String> instead.
    println!("first: {}", names[0]);

    // Iterate by reference so we don't move ownership out of the vector.
    for name in &names {
        println!("- {}", name);
    }

    // pop removes and returns the last element as Option<T>.
    // Empty vector → None. Otherwise → Some(value).
    if let Some(last) = names.pop() {
        println!("removed: {}", last);
    }
}

A few things deserve a closer look.

names[0] looks innocent but it can crash your program. If the vector is empty, indexing panics with index out of bounds: the len is 0 but the index is 0. Idiomatic Rust prefers .get(0), which returns Option<&T>. You then handle the None case explicitly. Indexing is fine when you've already checked the length or you're inside a loop bounded by 0..vec.len(). In doubt, use get.

The &names in the for-loop is doing real work. Without the &, you'd write for name in names and the loop would consume the vector, moving each String out one by one. After the loop, names would be unusable. With &names, you iterate by shared reference, and the vector is still yours afterwards. This is the same pattern you'll see everywhere in Rust: by default, things move; you opt into borrowing.

What "same type" really means

A Vec<T> holds elements all of the same type T. You can't put an i32 and a String in the same vector. This isn't arbitrary: the vector lays its elements out in a single contiguous chunk of memory, and the compiler needs to know exactly how big each slot is so it can compute offsets. If you genuinely need a heterogeneous list, you reach for an enum (each variant carries its own data, but they're all the same enum type) or a Vec<Box<dyn Trait>> (every element implements the same trait, accessed through a fat pointer).

// An enum lets one Vec hold "either a number or a label" without resorting
// to dynamic dispatch.
enum Item {
    Number(i32),
    Label(String),
}

fn main() {
    let items = vec![
        Item::Number(42),
        Item::Label(String::from("hello")),
        Item::Number(7),
    ];

    // Pattern match on each element to extract the right shape.
    for item in &items {
        match item {
            Item::Number(n) => println!("number: {}", n),
            Item::Label(s) => println!("label:  {}", s),
        }
    }
}

That's still a Vec<Item>, not a Vec<i32 or String>. The single-type rule is preserved; the enum just has internal flexibility.

How growth actually happens

You push elements. At some point, the vector's len reaches its capacity. The next push triggers a reallocation: Vec asks the allocator for a bigger block (typically double the current capacity), copies every element over, frees the old block, and continues. From the caller's perspective, push always "just works." The cost is hidden but real: a single push that triggers reallocation might copy thousands of bytes. Across a long sequence of pushes, the doubling rule guarantees the average cost per push stays constant. That's what "amortised O(1)" means.

If you know up front you'll be pushing a million items, Vec::with_capacity(1_000_000) skips all the intermediate reallocations. It's a free perf win when the size is predictable. When you have no idea, don't worry about it; the doubling strategy is already pretty good.

Common pitfalls

You forgot mut. The compiler refuses to let you push or pop:

error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable
 --> src/main.rs:3:5
  |
2 |     let v: Vec<i32> = Vec::new();
  |         - help: consider changing this to be mutable: `mut v`
3 |     v.push(1);
  |     ^^^^^^^^^ cannot borrow as mutable

Add mut. Rust mutability is opt-in by design.

You tried to index past the end. Runtime panic:

thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 5'

Switch to .get(i) and handle None, or check the length first.

You moved the vector into a for-loop and then tried to use it. Compile error: "value used after move." Loop with &vec to borrow instead.

You collected an iterator and the compiler can't infer the element type. Help it: let v: Vec<i32> = (0..10).collect(); or (0..10).collect::<Vec<i32>>().

When to reach for Vec vs alternatives

Vec<T> is the default. When you don't know what kind of collection you want, start with Vec. Push to the back, pop from the back, iterate, index. It's fast for everything you'd reasonably do.

If you also need to push and pop from the front efficiently, VecDeque<T> is the same idea with both ends open. See What is the difference between Vec and VecDeque.

If you need key-based lookup, that's HashMap<K, V>, not a vector.

If you only ever read the data and never modify it, an array [T; N] or a &[T] slice is even simpler and might avoid a heap allocation entirely.

LinkedList<T> exists in the standard library, but you almost never want it. Cache locality matters more than asymptotic cleverness for any list small enough to fit in a normal program. If you think you want a linked list, double-check that you actually do.

Where to go next

Vectors are the workhorse of Rust collections; understanding them deeply pays off everywhere else.

How to Use Vec (Vectors) in Rust: The Complete Guide

How to retain elements in Vec

How to split Vec into chunks