Rust's Vec::drain Early-Return Trap: The Iterator That Steals Everything When You Leave

2026-07-22

This MessageQueue is supposed to find and remove the first high-priority message, leaving every other message untouched so the next worker can process them. It compiles, it passes a unit test with one high-priority message at the front, and it ships. Two weeks later, users complain that low-priority messages are silently disappearing.

#[derive(PartialEq)]
enum Priority { Low, High }

struct Message {
    priority: Priority,
    body: String,
}

struct MessageQueue {
    messages: Vec<Message>,
}

impl MessageQueue {
    /// Remove and return the first High-priority message.
    /// All other messages should remain in the queue.
    fn take_priority(&mut self) -> Option<Message> {
        for msg in self.messages.drain(..) {
            if msg.priority == Priority::High {
                return Some(msg);
            }
        }
        None
    }
}

Queue starts as [Low, Low, High, Low, Low]. Call take_priority(). You get back the High message. The queue is now… empty. Every Low — including the two that came after the high-priority one and were never even iterated — is gone.

The Bug

Vec::drain(..) returns a Drain<'_, T> iterator, and that iterator's Drop impl is what actually mutates the vector. The contract is: when the Drain is dropped, every element in the drained range that hasn't been yielded yet is removed from the vec and destroyed in place. This is what makes drain exception-safe — the vec is guaranteed to be in a valid state no matter how the loop exits.

When we return Some(msg) from inside the loop, we're leaving the scope where the Drain lives. The compiler dutifully drops it, and its destructor eats the two Low messages we never touched. From the caller's perspective, they vanished.

The subtlety: this is not a bug in Drain. It's doing exactly what its type signature says. The bug is thinking of drain(..) as "iterate and remove as you go, so if I stop early, the rest stay." It doesn't work that way. drain(range) is a commitment to remove the entire range; the iterator is just how you get access to the elements before they die.

The same trap fires with break, with ? propagation on a fallible loop body, or with any panic inside the loop.

The Fix

Find the index first, then remove exactly that element:

fn take_priority(&mut self) -> Option<Message> {
    let idx = self.messages
        .iter()
        .position(|m| m.priority == Priority::High)?;
    Some(self.messages.remove(idx))
}

If you specifically want the "scan and remove one" pattern to be O(n) without a second pass, Vec::retain_mut with a captured flag works, though it's uglier. But do not reach for drain here — its whole purpose is bulk removal.

A useful mental rule: drain is for "I want to consume these elements and clear them from the vec." If your loop can decide midway that it's done, you don't want drain. You want indexed access, swap_remove, or retain.

Key Takeaway: Vec::drain's destructor removes every unyielded element in the range, so any early return, break, or ? inside the loop silently deletes the tail you thought you were preserving.

All newsletters