---
title: "22.1 Threads"
description: "Running code concurrently with thread::spawn, joining handles to wait for results, and why spawned closures usually need move."
url: "https://learnrust.net/chapter-22/threads/"
last_updated: "2026-06-13"
---

# 22.1 Threads

Everything you've written so far runs one instruction after another, top to bottom, a single line of execution. Modern computers have many processor cores, and a program can run several lines of execution *at the same time*, each on its own core. Each such line is a **thread**. Threads let a program do several things at once: download files while the interface stays responsive, process chunks of a big computation in parallel, serve many users simultaneously. This chapter is Rust's headline feature, "fearless concurrency", and it earns the name by making the worst concurrency bugs *compile errors*. We start with threads themselves.

Most beginner courses skip threads entirely, because in most languages concurrency is a minefield of subtle, intermittent bugs. Rust is the reason a gentle course can cover it: the same ownership and borrowing rules you've spent twelve chapters learning turn out to be exactly what's needed to make concurrency safe, and the compiler enforces them here too.

## Spawning a thread

The standard library's `thread::spawn` starts a new thread running a closure (chapter 19, now load-bearing):

```rust
use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..=5 {
            println!("spawned: {i}");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..=3 {
        println!("main: {i}");
        thread::sleep(Duration::from_millis(1));
    }
}
```

A possible run:

```
main: 1
spawned: 1
main: 2
spawned: 2
main: 3
spawned: 3
```

`thread::spawn` takes a closure and runs it on a new thread, *concurrently* with the code that follows. So `main` keeps going to its own loop while the spawned thread runs its loop, and the two interleave. The `sleep` calls just slow things down enough to see the interleaving. Run it several times and the exact order shifts, because the operating system schedules the two threads independently, and which one prints next is up to it. That non-determinism is the first thing to internalize about threads: you don't control the ordering unless you explicitly coordinate it.

Notice something unsettling about that output, though: the spawned thread printed only up to 3, not 5. That's the next problem.

## When main ends, everything ends

When `main` returns, the whole program exits, *immediately*, even if spawned threads are still working. In the run above, `main` finished its three iterations and returned, and the spawned thread was killed mid-loop before reaching `i = 4`. The spawned thread isn't guaranteed to finish, or even to start. This is rarely what you want. You need a way to say "wait for that thread to finish before moving on."

## Joining: waiting for a thread

`thread::spawn` returns a **`JoinHandle`**, a value representing the running thread. Calling `.join()` on it blocks, waits, until that thread finishes:

```rust
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("spawned: {i}");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..=3 {
        println!("main: {i}");
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
    println!("all threads done");
}
```

```
main: 1
spawned: 1
main: 2
spawned: 2
main: 3
spawned: 3
spawned: 4
spawned: 5
all threads done
```

Now `main` runs its loop, then hits `handle.join()`, which pauses `main` until the spawned thread has run all five iterations. Only then does `all threads done` print. The `join` returns a `Result` (the thread might have panicked), which we `unwrap` here. Where you place the `join` matters: putting it right after `spawn` (before `main`'s own loop) would make `main` wait for the whole spawned thread *before* starting its loop, serializing them and defeating the point. Join when you actually need the results, which is usually near the end.

## Capturing data: the move closure

A thread usually needs data from the code that spawned it. The obvious attempt fails instructively:

```rust
use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("from thread: {data:?}");   // error: may outlive `data`
    });

    handle.join().unwrap();
}
```

```
error[E0373]: closure may outlive the current function, but it borrows `data`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `data`
7 |         println!("from thread: {data:?}");
  |                                 ---- `data` borrowed here
  |
help: to force the closure to take ownership of `data` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++
```

Read what the compiler is worried about. The closure *borrows* `data`. But a spawned thread can outlive the function that created it, the thread is independent, and `main` might return while the thread is still running. If the thread borrowed `data` and `main` ended, dropping `data`, the thread would be left holding a dangling reference, exactly the use-after-free the borrow checker exists to prevent, now in a concurrent setting. The compiler can't prove the thread finishes before `data` is dropped, so it refuses.

And, as it does so well, it tells you the fix: `move`. Adding `move` (lesson [19.1](https://learnrust.net/chapter-19/closures/)) makes the closure take **ownership** of `data`, so the data moves into the thread and lives as long as the thread does:

```rust
use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("from thread: {data:?}");
    });

    handle.join().unwrap();
    // `data` is owned by the thread now; main can't use it here.
}
```

```
from thread: [1, 2, 3]
```

With `move`, `data` belongs to the thread. There's no borrow that could dangle, so the compiler is satisfied. This is why spawned closures are almost always `move` closures: the thread needs to own what it uses, because nobody can promise the spawner will outlive it. The same ownership rule from chapter 8, applied across a thread boundary, and the same dangling-reference protection from chapter 9.

> **Key insight**
>
> Threads don't introduce a new safety model; they reuse the ownership system you already know. "A spawned thread might outlive its spawner" is just "a value might outlive a reference to it" with a thread boundary in the middle, and `move` is the same answer as always: give the thread ownership so nothing dangles. This is the heart of fearless concurrency. The compiler checks thread safety with the exact rules that checked single-threaded memory safety.

## Quiz time

**Question #1**

What does `thread::spawn` do, and what happens to a spawned thread if `main` returns before it finishes?

<details class="solution">
<summary>Show solution</summary>

`thread::spawn` starts a new thread running the given closure, concurrently with the code that follows. If `main` returns before the spawned thread finishes, the whole program exits immediately and the spawned thread is killed mid-work, it isn't guaranteed to complete (or even start). To wait for it, keep the `JoinHandle` and call `.join()`.

</details>

**Question #2**

What does `handle.join()` do, and why does its placement matter?

<details class="solution">
<summary>Show solution</summary>

`.join()` blocks the current thread until the spawned thread finishes (and returns a `Result`, since the thread could have panicked). Placement matters because `join` is a wait: calling it immediately after `spawn` makes the current thread wait for the whole spawned thread before continuing, serializing them and defeating concurrency. You generally join later, when you actually need the thread's work to be done.

</details>

**Question #3**

Why does a closure passed to `thread::spawn` usually need `move`?

<details class="solution">
<summary>Show solution</summary>

Because a spawned thread can outlive the function that created it, so if its closure merely *borrowed* a local variable, that variable could be dropped while the thread is still using it, a dangling reference. The compiler rejects that (E0373). `move` makes the closure take ownership of the captured data, so the data lives in the thread for as long as the thread runs, and nothing can dangle. It's the chapter-8/9 ownership rules enforced across the thread boundary.

</details>

Threads can run independently, but real concurrent programs need them to *communicate*. The next lesson ([22.2](https://learnrust.net/chapter-22/message-passing-channels/)) covers channels: the message-passing approach where threads send ownership of values to each other, and the compiler ensures a value is never used after it's sent.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
