🛰️

そのFuture、ほんとに待つ必要ありますか?

に公開

はじめに

こんにちは!
ツクリンクにてモバイルアプリ開発を担当しておりますイノウエと申します。

なんだか未来を憂ってる未来人ぽいタイトルになってしまいましたが、最近の気づきを思考の整理と共にちょっとまとめてみました✏️

こんなこと書きまっせ

Future型の非同期処理の際、おまじないのようにとりあえずawaitを書いておくことはないでしょうか。実はすべてのFutureを待つ必要があるわけではありません。むしろ、不要なawaitがパフォーマンス低下やバグの原因になることもあります。

少し前に私自身、「なぜか値が更新されず期待通りに動作しない。。」とハマっていた時にこのawaitが肝だったことがあり、これ共有ぅぅぅと思った経緯がありました。

なのでこの記事で改めて「awaitすべきとき・すべきでないとき」を整理し、Futureをより正しく扱えるようになる気づきを得られる良い機会になればうれCです!

まず、Futureとは

Future<T> は、「いつか値 T を返す約束」を表す型です。APIの呼び出し、DBへのアクセスなど、結果が出るまで時間のかかる処理に使われることが多いのではないでしょうか。

Future<String> fetchUserName() async {
  final response = await http.get(Uri.parse('https://api.example.com/user'));
  return jsonDecode(response.body)['name'];
}

Future には主に3つの状態があります。

状態 意味
uncompleted まだ処理中
completed with value 正常に完了し値が返った
completed with error エラーで完了した

await はこの「約束」が果たされるまで、その関数の実行を一時停止します。Dartは非同期処理をシングルスレッドのイベントループで処理しているため、await 中もUIスレッドがブロックされることはありません。

リファレンス

Future Class

🙆🏽‍♂️awaitすべきところ

以下のケースでは、await は必須あるいは強く推奨されます。

1. 結果を使って次の処理をする場合

最も多い典型的なパターンではないでしょうか。前の処理の結果に依存する処理がある場合は、必ず待つ必要があります。

// ✅ ユーザーIDを取得してから、そのIDでプロフィールを取得する
final userId = await fetchUserId();
final profile = await fetchProfile(userId); // userIdが必要なので待つ

2. 完了を保証してから次に進みたい場合

データの保存や初期化処理など、「終わってから次へ」が重要な場面です。

// ✅ データ保存が完了してから画面遷移する
await prefs.setString('token', token);
Navigator.pushReplacementNamed(context, '/home');

おまけ. ウィジェットのdispose後に awaitが返ってきてしまう場合

ウィジェットが破棄されていたとしても、関数自体がキャンセルされるわけではありません。
await 後にウィジェットが破棄されていると、例えばsetStateの場合は例外を投げるので工夫が必要だったりしますね。

// ❌ dispose後にsetStateが呼ばれる可能性がある
Future<void> _load() async {
  final data = await fetchData();
  setState(() => _data = data); // ウィジェットが既にない場合にエラー
}

// ✅ マウント確認を挟む
Future<void> _load() async {
  final data = await fetchData();
  if (!mounted) return;
  setState(() => _data = data);
}

🙅🏽‍♂️awaitすべきではないところ

1. 結果を使わず、完了を待つ必要もない場合

ログの送信やアナリティクスのイベント送信など、「投げっぱなし」で構わない処理は待つ必要がありません。

// ❌ 無駄にawaitしている
Future<void> _onButtonTap() async {
  await analytics.logEvent('button_tapped'); // 結果も使わないし、待つ意味もない
  await doMainProcess();
}

// ✅ awaitしない
Future<void> _onButtonTap() async {
  analytics.logEvent('button_tapped'); // fire-and-forget(投げっぱなし)
  await doMainProcess();
}

await することで、analytics.logEvent が完了するまで doMainProcess の開始が遅れてしまいます。

2. 複数の独立した非同期処理を並列で実行したい場合

依存関係のない複数の処理を順番に await するのももったいないです。

// ❌ 合計 2秒かかる(直列)
final user    = await fetchUser();    // 1秒
final ranking = await fetchRanking(); // 1秒

// ✅ 合計 1秒で済む(並列)
final results = await Future.wait([
  fetchUser(),
  fetchRanking(),
]);
final user = results[0]; // Listで返ってくるのでインデックスで指定
final ranking = results[1];

Future.wait は複数の Future を並列実行し、すべてが完了したときに結果をまとめて返します。独立した処理であれば、積極的に活用しましょう。

ただし並列で実行しているどれかの関数がこけてしまうと厄介なので、以下のようにエラーを吸収するか、各関数内でtry-catchして捕捉できるようにしておくなどの工夫も必要だったりすると思います。

// 個別にエラーを吸収する例
final results = await Future.wait([
  fetchUser().catchError((_) => null as User?),   // こけたらnull
  fetchPosts().catchError((_) => null as List<Post>?), // こけたらnull
]);

awaitしない時にはどうするか

もしawaitをしない場合、処理はバックグラウンドで継続されます。状況に応じて適切な対処法を選ぶと良いと思います。

unawaited(); を使う

await を省略すると、unawaited_futures などのlintが警告を出すことがあります。「意図的にawaitしない」ことを明示するには unawaited() を使います。

import 'dart:async';

unawaited(analytics.logEvent('button_tapped'));

これにより、「見落としではなく、意図的な選択だ」と明示できます。

Future.wait(); で並列実行 + エラーハンドリング

try {
  final results = await Future.wait([
    fetchUser(),
    fetchPosts(),
  ]);
} catch (e) {
  // いずれかのFutureがエラーになると、ここでキャッチできる
  print('Error: $e');
}

なお Future.wait はデフォルトでeagerError: falseが設定されており、全Futureの完了を待ってからエラーを投げます。逆にeagerError: true を指定すると、最初のエラー発生時点で即座に例外を投げ、他のFutureの完了を待ちません。いずれにせよエラーがあれば catch に飛ぶため、個別に例外を補足したい場合は各Futureに .catchError() を付けてデフォルト値を返す方法が有効です。

then(); でコールバックスタイルに

どうしても async/await を使えない文脈(同期コンストラクタの中など)やワンライナーでスッキリ書きたい場面では then() を使うのも良いかもしれません。

fetchUser().then((user) {
  print('Hello, ${user.name}');
}).catchError((e) {
  print('Error: $e');
});

処理が嵩むとネストが深くなり可読性にも影響が出ると思うので、基本的にはasync-awaitで良いのではないかと思います。


まとめ

シチュエーション 推奨
結果を次の処理で使う await する
エラーを try-catch で処理する await する
処理完了を保証してから進む await する
結果も完了も不要(ログ送信など) await しない + unawaited()
複数の独立した処理を並列実行 Future.wait を使う
await 後に 何か(setStateとか) する mounted チェックを忘れずに

await は便利ですが、「待つ必要があるか?」を一度立ち止まって考える習慣を少しでも意識しておくと幸せに近づくのではないかなぁと思った今日この頃でした。

アディオス!
ぬえの先生

Discussion