---
title: "Rustのasync/await入門｜Futureとpollの仕組み、Tokioでの実行とよくあるエラー"
url: "https://www.issoh.co.jp/tech/details/8681/"
published: 2025-09-05
updated: 2026-10-25
categories: ["Rust"]
publisher: "株式会社一創"
---

# Rustのasync/await入門｜Futureとpollの仕組み、Tokioでの実行とよくあるエラー

Rustのasync/awaitは、I/O待ちの多い処理を同期コードに近い見た目で書くための構文です。ただしJavaScriptやPythonと違い、Rustの標準ライブラリには非同期タスクを動かすランタイムが入っていません。`async fn`を呼んだだけでは何も実行されず、Tokioなどのランタイムに渡して初めて動きます。構文でFutureを作ることと、ランタイム（executor）でFutureを進めることを分けて理解すると、呼び出しても処理が始まらない理由を整理できます。

本記事の掲載コードとエラーメッセージは、2026年10月25日時点の安定版Rust 1.99.1（2026年10月15日公開）とTokio 1.54.1（edition 2024）で実際にコンパイル・実行して取得したものです。未安定の機能はnightly（rustc 1.101.0-nightly、2026年10月23日ビルド）で動かしました。所有権や借用の基礎から確認したい場合は、先に[Rust入門｜環境構築から所有権・基本文法、受託開発での採用判断まで](/tech/details/15373/)を読んでおくと以降の説明が追いやすくなります。

## まとめ：Rustのasync/awaitの要点

- `async fn`は本体を実行せず、`Future`を返すだけです。`.await`するかランタイムに渡すまで1行も動きません。JavaScriptのPromiseが作った時点で動き出すのとは逆です。
- async/awaitはRust 1.39.0（2019年11月7日）で安定化しました。ランタイムは標準に含まれず、実務ではTokio（2026年10月時点で1.54.1）が事実上の標準です。async-stdは2025年3月に開発終了を告知しており、新規採用は避けてください。
- `tokio::join!`は1つのタスクの中で待ちを重ね、`tokio::spawn`は別タスクとして登録するので、マルチスレッドのランタイムでは並列に動く余地があります。CPU処理2本の実測では、spawnがjoin!のほぼ半分の時間で終わりました。
- トレイトの`async fn`はRust 1.75.0で標準化されましたが、`dyn Trait`にはできず、トレイトの宣言だけでは返すFutureが`Send`であることも保証されません。`dyn`が要るならasync-trait、`Send`が要るなら`-> impl Future + Send`の宣言かtrait-variantを使います。
- 非同期イテレータ（async gen）・AsyncDrop・`yield`を書けるコルーチンは、Rust 1.99.1でもnightly限定です。安定版ではfuturesクレートの`Stream`と、明示的な`async fn close`で代用します。

## async fnと.awaitの基本構文

### async fn・asyncブロック・.awaitの書き方

関数の前に`async`を付けると、戻り値が`impl Future<Output = 戻り値の型>`に変わります。呼び出し側は`.await`を後置して完了を待ちます。`.await`は`async fn`、`async`ブロック、非同期クロージャの中で書けます。

次の例は、200ミリ秒かかる処理を「順番に待つ」場合と「同時に待つ」場合を並べたものです。依存関係の追加は`cargo add tokio@1.54.1 --features full`で行いました。以降の例で使うfutures・async-trait・trait-variantは`cargo add futures@0.3.34 async-trait@0.1.92 trait-variant@0.1.3`で追加します（Cargoの操作は[Cargo（Rust）とは？インストールと使い方・主要コマンド一覧](/tech/details/9572/)を参照）。

```rust
use std::time::Duration;
use tokio::time::sleep;

async fn fetch_user(id: u32) -> String {
    sleep(Duration::from_millis(200)).await; // I/O待ちの代わり
    format!("user{id}")
}

#[tokio::main]
async fn main() {
    let a = fetch_user(1).await;          // 順番に待つ
    let b = fetch_user(2).await;
    println!("{a} {b}");

    let (c, d) = tokio::join!(fetch_user(3), fetch_user(4)); // 同時に待つ
    println!("{c} {d}");
}
```

```text
user1 user2
user3 user4
```

`.await`を2回続けると、1つ目が終わるまで2つ目は始まりません。`tokio::join!`は渡したFutureを同じタスク内で交互に進めるため、待ち時間が重なって合計がほぼ1回分に縮みます。ここで起きているのは並行（concurrent）であって、スレッドを増やす並列（parallel）ではありません。

### .awaitを付け忘れたasync fnが実行されない理由

Rustの`Future`は「遅延評価」です。呼び出した時点では中身の処理を始めず、誰かが進めるまで何も起きません。`.await`を付け忘れるとコンパイラは警告を出しますが、エラーにはならないので見落としやすい箇所です。

```rust
async fn greet() {
    println!("greet が実行された");
}

#[tokio::main]
async fn main() {
    greet(); // .await を付け忘れた
    println!("main の終わり");
}
```

```text
warning: unused implementer of `Future` that must be used
 --> src/bin/lazy.rs:7:5
  |
7 |     greet(); // .await を付け忘れた
  |     ^^^^^^^
  |
  = note: futures do nothing unless you `.await` or poll them
  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default

main の終わり
```

出力に「greet が実行された」は現れません。警告文の`` futures do nothing unless you `.await` or poll them ``がそのまま答えで、Futureは`.await`されるかpollされるまで何もしません。

### JavaScriptのPromiseとRustのFutureの違い

同じ形のコードをJavaScript（Node.js v26.5.0）で実行すると、出力の順番が逆になります。

```javascript
async function greet() {
  console.log("greet が実行された");
}

greet(); // await を付けない
console.log("main の終わり");
```

```text
greet が実行された
main の終わり
```

JavaScriptの`async function`は、呼んだ時点で最初の`await`まで本体が進みます。[MDNのPromiseコンストラクターの解説](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global%5FObjects/Promise/Promise)も、executorは「同期的に（Promise が構築されるとすぐに）呼び出されます」と書いています。実行役のイベントループも処理系に内蔵されているため、ランタイムを選ぶ工程がありません（Node.js側の仕組みは[Node.jsとは？仕組み・版の選び方・標準機能で動かす手順を実装目線で解説](/tech/details/17433/)を参照）。

もう1つの違いはキャンセルです。RustのFutureは、dropされるとそのFuture自体は以後pollされず、処理がその場で止まります。ただし`tokio::spawn`で別タスクにした処理は、`JoinHandle`をdropしても止まらず裏で動き続けます。次の例では100ミリ秒で打ち切ったあと600ミリ秒待っていますが、「job: 終了」は最後まで出力されません。

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

async fn job() {
    println!("job: 開始");
    tokio::time::sleep(Duration::from_millis(500)).await;
    println!("job: 終了"); // ここまで進まない
}

#[tokio::main]
async fn main() {
    let fut = job();
    let r = tokio::time::timeout(Duration::from_millis(100), fut).await;
    println!("timeout: {}", r.is_err());
    tokio::time::sleep(Duration::from_millis(600)).await;
    println!("main の終わり");
}
```

```text
job: 開始
timeout: true
main の終わり
```

| 観点       | JavaScriptのPromise             | RustのFuture                  |
| -------- | ------------------------------ | ---------------------------- |
| 処理の開始    | 生成した時点                         | .awaitかpollされた時点             |
| 実行する仕組み  | 処理系内蔵のイベントループ                  | Tokio等のランタイムを追加              |
| 途中で止める   | 標準の手段なし（AbortController等を別に渡す） | Future本体をdrop（spawn済みはabort） |
| awaitの位置 | 前置（await p）                    | 後置（f.await）                  |

## Futureトレイトとpollの仕組み

### Futureトレイトの定義とPoll::Pending

`std::future::Future`の定義は、属性を省くと次の数行しかありません。

```rust
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}
```

ランタイム（executor）は`poll`を呼び、結果が`Poll::Ready(値)`なら完了、`Poll::Pending`なら「まだ終わっていない」と受け取ります。`Pending`を返すFutureは、再開できる状態になったら`cx.waker()`で起こしてもらうよう登録しておく約束です。これを守らないFutureは、再度pollされる機会を得られず、処理が停止したままになるおそれがあります。

手でFutureを実装すると、この往復が見えます。3回`Pending`を返してから完了するFutureを、futuresクレートの最小executorである`futures::executor::block_on`で動かした例です。

```rust
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct CountDown(u32);

impl Future for CountDown {
    type Output = &'static str;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.0 == 0 {
            Poll::Ready("完了")
        } else {
            println!("poll: 残り{}", self.0);
            self.0 -= 1;
            cx.waker().wake_by_ref(); // もう一度 poll してほしいと executor に伝える
            Poll::Pending
        }
    }
}

fn main() {
    let result = futures::executor::block_on(CountDown(3));
    println!("{result}");
}
```

```text
poll: 残り3
poll: 残り2
poll: 残り1
完了
```

`wake_by_ref()`の行を消すと、`block_on`は1回目の`Pending`のあと起床通知を待ち続け、プログラムは終了しません。1回だけ小さなFutureを書きたいときは、Rust 1.64.0で安定化した`std::future::poll_fn`を使えば構造体を定義せずに済みます。

### コンパイラが生成する状態マシンとコルーチン

`async fn`は、コンパイラの内部で「コルーチン」に変換されます。Rustの[Unstable Bookのcoroutines項](https://doc.rust-lang.org/nightly/unstable-book/language-features/coroutines.html)は、コルーチンの現在の主な用途を「`async`/`await`と`gen`構文の実装プリミティブ」と説明し、コンパイラ内では状態マシンとして生成されると書いています。`.await`の位置ごとに状態が1つでき、その時点で生きている変数だけが状態の中に保存されます。

この仕組みのおかげで、Rustのasyncはタスクごとにスタックを確保するグリーンスレッド型と違い、Futureの大きさがコンパイル時に決まります。OSスレッドやグリーンスレッドとの対比は[グリーンスレッドとは？OSスレッドとの違いとN:1モデルの制約を実測で解説](/tech/details/6332/)で扱っています。`yield`を書けるコルーチン構文そのもの（`#![feature(coroutines)]`）はnightly限定で、後半の「nightlyで動かすコルーチン・async gen・AsyncDrop」の章で実際に動かした結果を載せています。

### 自己参照を持つFutureとPinによる固定

状態マシンは、自分自身のフィールドを指す参照を持つことがあります。`.await`をまたいで`let s = String::new(); let r = &s;`のような借用を保つと、Futureの中に「自分の一部への参照」が入るためです。この値がメモリ上で移動すると参照が壊れるので、`poll`は`Pin<&mut Self>`を受け取り、移動しないことを型で保証しています。

実務でPinを意識するのは、Futureやストリームを変数に入れて何度も`poll`する場面です。次のコードは`pin!`を付けないと`cannot be unpinned`（`Unpin`が実装されていない）のエラーになりました。Rust 1.68.0で安定化した`std::pin::pin!`を使えば、追加のヒープ確保なしでローカルに固定できます。非同期処理内では、その保存先は外側のFutureが置かれる場所によります。

```rust
use futures::stream::{self, StreamExt};
use std::pin::pin;

#[tokio::main]
async fn main() {
    let mut s = pin!(stream::iter(1..=3).then(async |n| n * 10));
    while let Some(v) = s.next().await {
        println!("{v}");
    }
}
```

## async/awaitを動かすランタイム：Tokioの使い方

### #\[tokio::main\]が展開されるコード

`main`関数は`async`にできません。`#[tokio::main]`は、Tokioのランタイムを組み立てて`async`な本体を`block_on`で実行するコードに書き換える手続き的マクロです（マクロの仕組みは[Rustプロシージャルマクロの仕組みを徹底解説: コンパイル時コード生成の流れと内部処理](/tech/details/9463/)を参照）。tokio-macrosのドキュメントが示す等価コードは次のとおりです。

```rust
fn main() {
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap()
        .block_on(async {
            println!("Hello world");
        })
}
```

既定はマルチスレッドのランタイムで、ワーカースレッド数はCPUコア数です（環境変数`TOKIO_WORKER_THREADS`で変更可）。`#[tokio::main(flavor = "current_thread")]`と書けば、呼び出し元の1スレッドだけで全タスクを回します。CLIツールやテストではcurrent\_threadで足りる場合が多くあります。ただし、この設定でも`tokio::spawn`は`Send`を要求し、`Send`でないタスクには`LocalSet`と`spawn_local`が必要です。

### std::thread::sleepとtokio::time::sleepの違い

async関数の中で`std::thread::sleep`を呼ぶと、そのタスクだけでなくスレッドごと止まります。同じ1秒待ちをcurrent\_threadのランタイムで2つ同時に走らせ、所要時間を比べました。

```rust
use std::time::{Duration, Instant};

async fn std_sleep(id: u32) {
    std::thread::sleep(Duration::from_secs(1)); // スレッドごと止める
    println!("std  {id}");
}

async fn tokio_sleep(id: u32) {
    tokio::time::sleep(Duration::from_secs(1)).await; // タスクだけ止める
    println!("tokio {id}");
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let t = Instant::now();
    tokio::join!(std_sleep(1), std_sleep(2));
    println!("std::thread::sleep: {:.1}秒", t.elapsed().as_secs_f64());

    let t = Instant::now();
    tokio::join!(tokio_sleep(1), tokio_sleep(2));
    println!("tokio::time::sleep: {:.1}秒", t.elapsed().as_secs_f64());
}
```

```text
std  1
std  2
std::thread::sleep: 2.1秒
tokio 2
tokio 1
tokio::time::sleep: 1.0秒
```

`std::thread::sleep`では2本が順番に実行されて2.1秒、`tokio::time::sleep`では待ちが重なって1.0秒でした。ファイル読み込みや同期版のDBドライバなど、中でスレッドを止める関数はすべて同じ問題を起こします。避けられない同期処理は`tokio::task::spawn_blocking`で専用のスレッドプール（既定の上限512本）へ逃がします。

### tokio::join!とtokio::spawnの違い：並行と並列の実測

`tokio::join!`は、渡したFutureを1つのタスクの中で交互にpollします。I/Oの待ち時間は重なりますが、使うスレッドは1本です。`tokio::spawn`はFutureを別タスクとして登録するため、マルチスレッドのランタイムでは空いているワーカースレッドで並列に実行されることがあります（どのワーカーで動くかはスケジューラが決め、`current_thread`では並列になりません）。差がはっきり出るのは、`.await`を挟まないCPU処理です。

```rust
use std::time::Instant;

fn busy() -> u64 {
    (0..std::hint::black_box(1_000_000_000u64)).fold(0, |a, x| a ^ x.wrapping_mul(31))
}

async fn cpu_job() -> u64 {
    busy() // .await を挟まないCPU処理
}

#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
    let t = Instant::now();
    let (a, b) = tokio::join!(cpu_job(), cpu_job());
    println!("join!  : {:.2}秒 ({})", t.elapsed().as_secs_f64(), a == b);

    let t = Instant::now();
    let h1 = tokio::spawn(cpu_job());
    let h2 = tokio::spawn(cpu_job());
    let (a, b) = (h1.await.unwrap(), h2.await.unwrap());
    println!("spawn  : {:.2}秒 ({})", t.elapsed().as_secs_f64(), a == b);
}
```

```text
join!  : 0.57秒 (true)
spawn  : 0.28秒 (true)
```

Intel Core i9-9880H（16論理コア）でreleaseビルドを3回実行すると、join!は0.55〜0.86秒、spawnは0.28〜0.42秒で、spawnが毎回ほぼ半分でした。join!の2本は同じスレッドで順番に計算され、この実測ではspawnの2本が2つのワーカーで同時に計算されたためです。I/O待ちを重ねるだけならjoin!で足ります。数秒かかる計算はspawnでもワーカーを占有して他のタスクを止めるので、`spawn_blocking`かrayonに渡します。

### tokio::spawnのJoinHandleとJoinSetによる回収

`tokio::spawn`はFutureを独立したタスクとしてランタイムに登録し、`JoinHandle`を返します。`.await`すると`Result<T, JoinError>`が返り、タスク内のpanicは`Err`として受け取れます。複数のタスクを投げて終わった順に回収するなら`JoinSet`が便利です。

```rust
use std::time::Duration;
use tokio::task::JoinSet;

#[tokio::main]
async fn main() {
    let mut set = JoinSet::new();
    for id in [3u64, 1, 2] {
        set.spawn(async move {
            tokio::time::sleep(Duration::from_millis(id * 100)).await;
            id
        });
    }
    while let Some(res) = set.join_next().await {
        println!("完了: {}", res.unwrap());
    }
}
```

```text
完了: 1
完了: 2
完了: 3
```

登録順は3→1→2ですが、回収は待ち時間の短い順になりました。`JoinSet`をdropすると、残っているタスクはすべて中断されます。

### timeoutとselect!によるキャンセル

前述のとおり、Futureはdropすると以後進みません（spawn済みのタスクを止めるには`JoinHandle::abort`を呼びます）。`tokio::time::timeout`は、期限までに終わらなかったFutureをdropして`Err(Elapsed)`を返します。期限の判定は内側のFutureをpollしたあとに行うため、`.await`を挟まず実行権を返さない処理は、期限を過ぎても`Ok`で完了することがあります（[tokio::time::timeoutのドキュメント](https://docs.rs/tokio/1.54.1/tokio/time/fn.timeout.html)）。

```rust
use std::time::Duration;
use tokio::time::{sleep, timeout};

async fn slow() -> &'static str {
    sleep(Duration::from_secs(3)).await;
    "slow の結果"
}

#[tokio::main]
async fn main() {
    match timeout(Duration::from_secs(1), slow()).await {
        Ok(v) => println!("{v}"),
        Err(e) => println!("タイムアウト: {e}"),
    }
}
```

```text
タイムアウト: deadline has elapsed
```

`tokio::select!`は、完了した結果がパターンに一致した枝のハンドラを実行します。所有している他のFutureは破棄しますが、借用して渡したFuture本体は残ります。

```rust
use std::time::Duration;
use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel::<&str>(8);
    tokio::spawn(async move {
        tokio::time::sleep(Duration::from_millis(300)).await;
        tx.send("メッセージ").await.unwrap();
    });

    tokio::select! {
        Some(msg) = rx.recv() => println!("受信: {msg}"),
        _ = tokio::time::sleep(Duration::from_millis(100)) => println!("100msで打ち切り"),
    }
}
```

```text
100msで打ち切り
```

select!で注意するのは、途中まで進んだ処理が捨てられることです。Tokioは各メソッドのドキュメントに「Cancel safety」の節を設けており、`mpsc::Receiver::recv`は「cancel safe（負けた枝では1件も受信していない）」、`AsyncReadExt::read_exact`は「not cancel safe（読みかけのデータが失われうる）」と明記しています。select!の枝に置く前に、その節を確認してください。

## async-stdの開発終了とランタイムの選び方

Tokioとasync-stdを並べて比較する解説は、もう前提が成り立ちません。async-stdのGitHubリポジトリのREADME冒頭は「async-std has been discontinued; use smol instead」となっており、2025年3月15日のv1.13.1で開発終了を告知しました。crates.ioの説明文も`` Deprecated in favor of `smol` ``に変わっています。

| ランタイム             | 最新版（2026年10月25日） | 位置づけ                           |
| ----------------- | ---------------- | ------------------------------ |
| Tokio             | 1.54.1           | 事実上の標準。Axum・tonic・reqwest等が前提  |
| smol              | 2.0.2            | 小型のランタイム。async-stdの移行先         |
| futures::executor | futures 0.3.34   | block\_on・LocalPool等。I/Oドライバなし |
| async-std         | 1.13.2           | 開発終了。新規採用しない                   |

Axum・tonic・reqwestを中心に構成するなら、依存関係を揃えやすいTokioを推奨します。Axumもtonicも内部でTokioに依存しており、別ランタイムを選ぶと互換レイヤーが必要になります（[Axumとは？Rust製Webフレームワークの特徴・使い方・Actix Web/Rocket比較を解説](/tech/details/7602/)、[gRPC Rustの実装｜tonic 0.14のクレート分割とTLS・相互接続を解説](/tech/details/17215/)）。Tokioは固定の公開日程を設けず、通常は月1回程度マイナー版を公開しています。2026年10月19日の1.54.0でMSRV（対応する最低のRust）がRust 1.85に上がりました（[Tokio 1.54.0のリリースノート](https://github.com/tokio-rs/tokio/releases/tag/tokio-1.54.0)）。版を固定したい場合は、LTSの`1.51.x`（2027年3月まで）か`1.53.x`（2027年9月まで）を`tokio = "~1.53"`のようにチルダ指定します。2系統ともMSRVはRust 1.71のままで、2026年10月13日に1.51.6と1.53.3が出ています。LTSの一覧は[TokioのGitHubリポジトリのREADME](https://github.com/tokio-rs/tokio#bug-patching-policy)で確認してください。

## async fn in traitとasync-traitクレートの使い分け

### Rust 1.75で標準化されたasync fn in traitとdyn非対応

Rust 1.75.0（2023年12月28日）から、トレイトのメソッドに直接`async fn`を書けます。以前は必須だったasync-traitクレートがなくても、ジェネリクス経由の静的ディスパッチなら動きます。一方で`Box<dyn Trait>`にしようとすると、次のエラーになります。

```rust
trait Fetcher {
    async fn fetch(&self) -> String;
}

struct Api;
impl Fetcher for Api {
    async fn fetch(&self) -> String { "ok".into() }
}

fn main() {
    let _list: Vec<Box<dyn Fetcher>> = vec![Box::new(Api)];
}
```

```text
error[E0038]: the trait `Fetcher` is not dyn compatible
  --> src/bin/dyntrait.rs:11:28
   |
11 |     let _list: Vec<Box<dyn Fetcher>> = vec![Box::new(Api)];
   |                            ^^^^^^^ `Fetcher` is not dyn compatible
   |
note: for a trait to be dyn compatible it needs to allow building a vtable
      for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
  --> src/bin/dyntrait.rs:2:14
   |
 1 | trait Fetcher {
   |       ------- this trait is not dyn compatible...
 2 |     async fn fetch(&self) -> String;
   |              ^^^^^ ...because method `fetch` is `async`
   = help: consider moving `fetch` to another trait
   = help: only type `Api` implements `Fetcher`; consider using it directly instead.
```

`async fn`が返すFutureの型は実装ごとに違い、vtableに1つの型として載せられないためです。

### Sendを要求する場面の書き換えとtrait-variant

もう1つの制約は`Send`です。トレイト定義の`async fn`は、返すFutureが`Send`かどうかを約束しません。そのためジェネリクスで受け取った実装を`tokio::spawn`に渡すと失敗します。

```rust
trait Fetcher { async fn fetch(&self) -> String; }

async fn run<F: Fetcher + Send + Sync + 'static>(f: &'static F) {
    tokio::spawn(async move { f.fetch().await });
}
fn main() {}
```

```text
error: future cannot be sent between threads safely
   --> src/bin/nosend.rs:4:5
    |
  4 |     tokio::spawn(async move { f.fetch().await });
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ future created by async block is not `Send`
    |
    = help: within `{async block@src/bin/nosend.rs:4:18: 4:28}`, the trait `Send` is not implemented for `impl Future<Output = String>`
note: future is not `Send` as it awaits another future which is not `Send`
   --> src/bin/nosend.rs:4:31
    |
  4 |     tokio::spawn(async move { f.fetch().await });
    |                               ^^^^^^^^^ await occurs here on type `impl Future<Output = String>`, which is not `Send`
help: `Send` can be made part of the associated future's guarantees for all implementations of `Fetcher::fetch`
    |
  1 - trait Fetcher { async fn fetch(&self) -> String; }
  1 + trait Fetcher { fn fetch(&self) -> impl std::future::Future<Output = String> + Send; }
    |
```

rustcのhelpが示すとおり、トレイト側の宣言を`fn fetch(&self) -> impl Future<Output = String> + Send;`に書き換えれば、実装側は`async fn`のままで`tokio::spawn`に渡せます。

```rust
use std::future::Future;

trait Fetcher {
    fn fetch(&self) -> impl Future<Output = String> + Send;
}

struct Api;
impl Fetcher for Api {
    async fn fetch(&self) -> String { "api".into() }
}

async fn run<F: Fetcher + Sync + 'static>(f: &'static F) {
    tokio::spawn(async move { println!("{}", f.fetch().await) }).await.unwrap();
}

#[tokio::main]
async fn main() {
    static API: Api = Api;
    run(&API).await;
}
```

```text
api
```

この宣言にすると、`Rc`を握ったまま`.await`する実装のように、Sendでない実装はコンパイルできなくなります。呼び出し側で`F::fetch(..): Send`と境界を書くreturn type notation（[tracking issue #109417](https://github.com/rust-lang/rust/issues/109417)）は、2026年10月時点でnightly限定です。

Send版と非Send版の両方を公開したいときの手段として、Rustの公式ブログ（2023年12月21日）はrust-lang製の`trait-variant`クレートを案内しています。`#[trait_variant::make]`を付けると、戻り値に`Send`を付けた版のトレイトが自動生成されます。

```rust
#[trait_variant::make(Fetcher: Send)]
trait LocalFetcher {
    async fn fetch(&self) -> String;
}

struct Api;
impl Fetcher for Api {
    async fn fetch(&self) -> String { "api".into() }
}

async fn run<F: Fetcher + Sync + 'static>(f: &'static F) {
    tokio::spawn(async move { println!("{}", f.fetch().await) }).await.unwrap();
}

#[tokio::main]
async fn main() {
    static API: Api = Api;
    run(&API).await;
}
```

### dyn Traitが必要な場合のasync-trait

プラグインのように実装を実行時に切り替えたい場合は、今もasync-traitクレート（0.1.92）を使います。トレイトと実装の両方に属性を付けるだけで、先ほどエラーになった`Vec<Box<dyn Fetcher>>`が通ります。

```rust
#[async_trait::async_trait]
trait Fetcher {
    async fn fetch(&self) -> String;
}

struct Api;

#[async_trait::async_trait]
impl Fetcher for Api {
    async fn fetch(&self) -> String { "api".into() }
}

struct Cache;

#[async_trait::async_trait]
impl Fetcher for Cache {
    async fn fetch(&self) -> String { "cache".into() }
}

#[tokio::main]
async fn main() {
    let list: Vec<Box<dyn Fetcher>> = vec![Box::new(Api), Box::new(Cache)];
    for f in &list {
        println!("{}", f.fetch().await);
    }
}
```

```text
api
cache
```

代わりに、async-traitはメソッドの戻り値を`Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>`に書き換えるため、呼び出しのたびにヒープ確保が1回発生します。静的ディスパッチで足りる箇所まで付けて回る必要はありません。

### asyncクロージャとAsyncFn（Rust 1.85）

Rust 1.85.0（2025年2月20日）で`async |x| ...`形式のクロージャと、それを受け取るための`AsyncFn`・`AsyncFnMut`・`AsyncFnOnce`トレイトが安定化しました。以前の`|x| async move { ... }`と違い、クロージャが捕捉した変数を返すFutureの中から借用できます。

```rust
async fn call_twice<F: AsyncFn(u32) -> u32>(f: F) -> u32 {
    f(1).await + f(2).await
}

#[tokio::main]
async fn main() {
    let base = 10;
    let add = async |x: u32| x + base; // Rust 1.85で安定化した async クロージャ
    println!("{}", call_twice(add).await);
}
```

```text
23
```

## nightlyで動かすコルーチン・async gen・AsyncDrop

2026年10月25日時点の安定版Rust 1.99.1には、`yield`を書けるコルーチン、非同期版のイテレータ、非同期のデストラクタのどれもありません。いずれもnightly限定の機能で、安定版で`#![feature(...)]`を書くと`` error[E0554]: `#![feature]` may not be used on the stable release channel ``で止まります。ここではnightly（rustc 1.101.0-nightly、2026年10月23日ビルド）で実際に動かした結果を載せます。

| 機能                   | feature名               | tracking issue |
| -------------------- | ---------------------- | -------------- |
| yieldを書けるコルーチン       | coroutines             | #43122         |
| genブロック              | gen\_blocks            | #117078        |
| AsyncIterator        | async\_iterator        | #79024         |
| AsyncDrop            | async\_drop            | #126482        |
| return type notation | return\_type\_notation | #109417        |

### yieldを書けるコルーチンとgenブロック

Rustのコルーチンは、呼び出し側が`resume`を呼ぶたびに、前回の`yield`の続きから実行を進める構文です。`#[coroutine]`を付けたクロージャの中で`yield`した値は`CoroutineState::Yielded`として返り、本体の最後まで進むと`Complete`になります。

```rust
#![feature(coroutines, coroutine_trait, stmt_expr_attributes)]
use std::ops::{Coroutine, CoroutineState};
use std::pin::pin;

fn main() {
    let mut co = pin!(#[coroutine] || {
        yield 1;
        yield 2;
        "終わり"
    });
    loop {
        match co.as_mut().resume(()) {
            CoroutineState::Yielded(v) => println!("Yielded({v})"),
            CoroutineState::Complete(r) => { println!("Complete({r})"); break; }
        }
    }
}
```

```text
Yielded(1)
Yielded(2)
Complete(終わり)
```

イテレータを作るだけなら、`gen`ブロックのほうが短く書けます。`gen`は[Rust 2024 editionで予約語](https://doc.rust-lang.org/edition-guide/rust-2024/gen-keyword.html)になっており、edition 2024のコードでは`gen`という名前の変数や関数に`r#gen`と書く必要があります。

```rust
#![feature(gen_blocks)]

fn evens(max: u32) -> impl Iterator<Item = u32> {
    gen move {
        for n in 0..=max {
            if n % 2 == 0 { yield n; }
        }
    }
}

fn main() {
    let v: Vec<u32> = evens(8).collect();
    println!("{v:?}");
}
```

```text
[0, 2, 4, 6, 8]
```

### async genブロックとAsyncIterator

`async gen`ブロックは、中で`.await`と`yield`の両方を書けるブロックで、`std::async_iter::AsyncIterator`を返します。`for await`の構文は使わず、`poll_next`を`poll_fn`で包んで1件ずつ取り出しました。

```rust
#![feature(async_iterator, gen_blocks)]
use std::async_iter::AsyncIterator;
use std::future::poll_fn;
use std::pin::pin;
use std::time::Duration;

#[tokio::main]
async fn main() {
    let mut s = pin!(async gen {
        for i in 1..=3 {
            tokio::time::sleep(Duration::from_millis(100)).await;
            yield i * 10;
        }
    });
    while let Some(v) = poll_fn(|cx| s.as_mut().poll_next(cx)).await {
        println!("{v}");
    }
}
```

```text
10
20
30
```

安定版で同じことをするなら、futuresクレートの`Stream`トレイトと`StreamExt`を使います。前述の`pin!`の例がその形です。tokio-streamやtonicのストリーム型も、この`Stream`トレイトを実装しています。

### AsyncDropの挙動と安定版での後片付け

AsyncDropは、値が破棄されるときに`.await`を含む後片付けを走らせるトレイトです。接続のクローズを模した型で試しました。

```rust
#![feature(async_drop)]

use std::future::AsyncDrop;
use std::pin::Pin;

struct Conn(&'static str);

impl Drop for Conn {
    fn drop(&mut self) { println!("{}: 同期のdrop", self.0); }
}

impl AsyncDrop for Conn {
    async fn drop(self: Pin<&mut Self>) {
        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
        println!("{}: 非同期のdropが完了", self.0);
    }
}

async fn work() {
    let _c = Conn("接続A");
    println!("work の本体");
}

#[tokio::main]
async fn main() {
    work().await;
    println!("main の終わり");
}
```

```text
warning: the feature `async_drop` is incomplete and may not be safe to use and/or cause compiler crashes
 --> src/bin/asyncdrop.rs:1:12
  |
1 | #![feature(async_drop)]
  |            ^^^^^^^^^^
  |
  = note: see issue #126482 <https://github.com/rust-lang/rust/issues/126482> for more information
  = note: `#[warn(incomplete_features)]` on by default
```

```text
work の本体
接続A: 非同期のdropが完了
main の終わり
```

async関数の中で落ちた「接続A」では、同期の`Drop`は呼ばれず非同期のdropだけが走りました。同じ型を同期の`fn main`の中で落とすと、同期の`Drop`が呼ばれて「接続B: 同期のdrop」が出力されます。コンパイラは`async_drop`を「incomplete（未完成でコンパイラがクラッシュする可能性がある）」と警告しており、本番のコードで使う段階ではありません。

安定版の`Drop::drop`の中では`.await`できません。非同期の後片付けが要る型は、`async fn close(self)`のような明示的なメソッドを用意し、呼び出し側で`.await`する設計にします。

## async Rustでつまずくコンパイルエラーと直し方

ここからは、実際に遭遇しやすいエラーを再現し、メッセージと直し方を対にしてまとめます。エラー文は英語のまま長く出ますが、見るべき行は決まっています。掲載したエラーは、ローカルのパスを含む`` note: required by a bound in `tokio::spawn` ``以降の行と末尾の要約行を省いています。

### MutexGuardを.awaitの先まで持つとSendエラー

```rust
use std::sync::{Arc, Mutex};

async fn save(_v: u32) {}

#[tokio::main]
async fn main() {
    let counter = Arc::new(Mutex::new(0u32));
    let c = counter.clone();
    tokio::spawn(async move {
        let mut guard = c.lock().unwrap();
        *guard += 1;
        save(*guard).await; // ガードを保持したまま .await
    });
}
```

```text
error: future cannot be sent between threads safely
   --> src/bin/mutex.rs:9:5
    |
  9 | /     tokio::spawn(async move {
 10 | |         let mut guard = c.lock().unwrap();
 11 | |         *guard += 1;
 12 | |         save(*guard).await; // ガードを保持したまま .await
 13 | |     });
    | |______^ future created by async block is not `Send`
    |
    = help: within `{async block@src/bin/mutex.rs:9:18: 9:28}`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, u32>`
note: future is not `Send` as this value is used across an await
   --> src/bin/mutex.rs:12:22
    |
 10 |         let mut guard = c.lock().unwrap();
    |             --------- has type `std::sync::MutexGuard<'_, u32>` which is not `Send`
 11 |         *guard += 1;
 12 |         save(*guard).await; // ガードを保持したまま .await
    |                      ^^^^^ await occurs here, with `mut guard` maybe used later
```

読むべきは`` note: future is not `Send` as this value is used across an await ``の行です。`std::sync::MutexGuard`は`Send`ではなく、それを保持したまま`.await`するとFuture全体が`Send`でなくなります。値を取り出したらブロックを閉じてガードを先に落とします。

```rust
tokio::spawn(async move {
    let v = {
        let mut guard = c.lock().unwrap();
        *guard += 1;
        *guard
    }; // ここでガードを落とす
    save(v).await;
}).await.unwrap();
```

ロックを握ったまま`.await`する設計がどうしても必要なら、`tokio::sync::Mutex`を使います。ただし標準のMutexより遅いため、Tokioのドキュメントも「ロックを`.await`の先まで持つ必要が無ければ標準のMutexでよい」という立場です。

### spawnに借用を渡すとE0373

```rust
#[tokio::main]
async fn main() {
    let name = String::from("issoh");
    let handle = tokio::spawn(async {
        println!("{name}");
    });
    handle.await.unwrap();
}
```

```text
error[E0373]: async block may outlive the current function, but it borrows `name`, which is owned by the current function
 --> src/bin/borrow.rs:4:31
  |
4 |     let handle = tokio::spawn(async {
  |                               ^^^^^ may outlive borrowed value `name`
5 |         println!("{name}");
  |                    ---- `name` is borrowed here
  |
  = note: async blocks are not executed immediately and must either take a reference or ownership of outside variables they use
help: to force the async block to take ownership of `name` (and any other referenced variables), use the `move` keyword
  |
4 |     let handle = tokio::spawn(async move {
  |                                     ++++
```

`tokio::spawn`のタスクは呼び出し元より長生きする可能性があるので、ローカル変数を借用できません（`'static`要件）。`async move`で所有権ごと渡すか、複数のタスクで共有するなら`Arc`に包んで`clone`してから渡します。

### async fnの再帰でE0733

```text
error[E0733]: recursion in an async fn requires boxing
 --> src/bin/recursion.rs:1:1
  |
1 | async fn fib(n: u32) -> u64 {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 |     if n < 2 { return n as u64; }
3 |     fib(n - 1).await + fib(n - 2).await
  |     ---------------- recursive call here
  |
  = note: a recursive `async fn` call must introduce indirection such as `Box::pin` to avoid an infinitely sized future
```

Futureは状態をすべて自分の中に持つため、自分自身を含むと大きさが無限になります。Rust 1.77.0以降は、再帰呼び出しを`Box::pin`で包んでヒープに逃がせば`async fn`のまま書けます（値がヒープへ移る条件は[Rustのスタックとヒープ｜値がヒープへ脱出する条件とメモリ領域の使い分け](/tech/details/2814/)を参照）。

```rust
async fn fib2(n: u32) -> u64 {
    if n < 2 { return n as u64; }
    Box::pin(fib2(n - 1)).await + Box::pin(fib2(n - 2)).await
}
```

### 非同期の中でblock\_onするとpanic

同期関数から非同期関数を呼びたくなり、`#[tokio::main]`の内側で別途作成したTokioランタイムの`block_on`を直接呼ぶと、コンパイルは通っても実行時にpanicします。

```text
Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.
```

非同期の文脈にいるなら`.await`で呼び、同期処理を挟みたいなら`spawn_blocking`を使います。Tokioの`block_on`は非同期処理を実行中のスレッドから直接呼べませんが、`spawn_blocking`内などから`Handle::block_on`で非同期処理へ戻ることはできます。

## async/awaitを使わないほうがよい場面

asyncが効くのは、待ち時間の大半がネットワークやディスクのI/Oで、同時に多数の接続をさばく処理です。逆の条件では導入しないほうが保守しやすくなります。

- 画像変換や圧縮などCPUを使い切る処理：Futureを分割しても計算量は減らず、ワーカースレッドを占有して他のタスクを止めます。rayonなどのスレッドプールか`spawn_blocking`に任せます。
- 数本のファイルを順に読むだけのCLI：待ち時間を重ねる処理がなければ、ランタイムを追加しても高速化は期待しにくく、構成が複雑になります。
- 処理の大半が同期APIへの逐次呼び出しで、他の非同期処理と並行させる必要がない場合：`spawn_blocking`との橋渡しが増えるため、同期構成を優先します。

同期と非同期の選び方を言語横断で整理したい場合は[非同期処理とは？同期処理との違いから実装方式まで実装者目線で解説](/tech/details/13590/)が参考になります。Rustのasyncはスレッドを作らずに状態マシンで待ちを重ねる設計で、その恩恵は[ゼロコスト抽象化とは？Rustで抽象度と実行速度を両立する仕組み](/tech/details/4440/)で説明している考え方の延長にあります。同時接続の多いAPIサーバーやバッチの並行化をRustで組む案件は、[フルスクラッチ開発](/service/system/fullscratch/)として要件の整理から対応しています。

## よくある質問

### Rustのawaitはどう書きますか？

`future.await`のように、式の後ろに`.await`を付けます。JavaScriptやPythonの前置`await`と違い後置構文なので、`client.get(url).send().await?.json().await?`のようにメソッドチェーンの途中に挟めます。書けるのは`async fn`、`async`ブロック、非同期クロージャの中です。

### Rustの標準ライブラリだけでasyncは動きますか？

標準ライブラリにあるのは`Future`トレイトや`Waker`などの部品だけで、完成品のexecutorも非同期I/Oも含まれていません。部品を組み合わせて自作のexecutorを書けば動かせますが、通常はTokioやsmolのようなランタイムか、futuresクレートの`futures::executor::block_on`を使います。

### async-stdはまだ使えますか？

既存コードはビルドできますが、2025年3月のv1.13.1で開発終了が告知され、公式にsmolへの移行が推奨されています。新規のプロジェクトではTokioかsmolを選んでください。

### Rustのasync traitは標準で書けますか？

Rust 1.75.0以降は、トレイトのメソッドに`async fn`を直接書けます。ただし`dyn Trait`として使えない点と、トレイトの宣言だけでは戻り値のFutureが`Send`であることを保証できない点は残っています。前者はasync-traitクレートで補い、後者はトレイト側を`fn fetch(&self) -> impl Future<Output = String> + Send`の形で宣言するか、trait-variantクレートで補います。

### Rustのasyncとコルーチンはどう関係していますか？

`async fn`はコンパイラ内部でコルーチンに変換され、`.await`の位置で区切られた状態マシンになります。一方、`yield`で値を返す汎用コルーチン構文と`Coroutine`トレイトは、2026年10月時点（Rust 1.99.1）でもnightly限定（`#![feature(coroutines)]`、tracking issue #43122）で、安定版で書くと`error[E0554]`になります。

## 関連記事

- [Rust入門｜環境構築から所有権・基本文法、受託開発での採用判断まで](/tech/details/15373/)
- [非同期処理とは？同期処理との違いから実装方式まで実装者目線で解説](/tech/details/13590/)
- [Axumとは？Rust製Webフレームワークの特徴・使い方・Actix Web/Rocket比較を解説](/tech/details/7602/)
- [グリーンスレッドとは？OSスレッドとの違いとN:1モデルの制約を実測で解説](/tech/details/6332/)
- [gRPC Rustの実装｜tonic 0.14のクレート分割とTLS・相互接続を解説](/tech/details/17215/)

---

出典: [Rustのasync/await入門｜Futureとpollの仕組み、Tokioでの実行とよくあるエラー](<https://www.issoh.co.jp/tech/details/8681/>)（株式会社一創）
