Codex と Claude のレビューで浮かんだ「処理中フラグと try/finally」の観察を、勉強のためにまとめたものです。
綴Photo のモバイル版で、「写真を『また今度』にする」操作が、ある条件でそれ以降ずっと固まる可能性のあるコードが見つかった。原因は try/finally の置き方ひとつ。引っかかるのは、try の手前に early return が1行紛れただけで finally がすり抜ける、という見落としやすい形だった。
この記事で扱う観察ポイント
- 「処理中フラグ」で二重実行を防ぐ定番パターン
- フラグを立てた後・try に入る前に early return すると、finally が走りません
- 同じ関数内の似た処理は安全だった、という非対称
処理中フラグが解放されないまま残る
コードで起きていたこと
問題のコードは、ざっくりこういう形だった。
Future<void> passCurrent(Photo photo) async {
if (sessionActionInProgress) return; // 処理中ならガード
setState(() => sessionActionInProgress = true); // フラグを立てる
final passKey = photo.assetId ?? photo.localPath ?? '';
if (passKey.isEmpty) return; // ← ここ。try の「外」で return している
try {
await db.passPhoto(passKey);
removeCurrentPhoto();
} finally {
sessionActionInProgress = false; // 必ず false に戻す(はずだった)
}
}passKey が空になるのは、写真の ID も保存パスも両方空という稀なケースだが、起きると体験としては「画面が固まった」と感じる致命的な方向のバグだ。
なぜそうなりやすいか
true にする行と finally で false にする行の間に、別の早期 return が紛れ込むと起きます。「フラグを立てる → try → finally で戻す」という流れがある状態で、後から「空チェック」を足すと、setState(true) の直後・try の手前に置くのが順序として自然に見えてしまいます。
改善の方向
フラグを立てる前に、空チェックを済ませます。
Future<void> passCurrent(Photo photo) async {
if (sessionActionInProgress) return;
final passKey = photo.assetId ?? photo.localPath ?? '';
if (passKey.isEmpty) return; // ← フラグを立てる「前」に移す
setState(() => sessionActionInProgress = true);
try {
await db.passPhoto(passKey);
removeCurrentPhoto();
} finally {
sessionActionInProgress = false;
}
}原則は「フラグを立てたら、その後の全ての離脱経路が finally を通るようにします」。立てる前に弾けるチェックは、立てる前に済ませます。
学びポイント
- フラグを立てた後に増やした条件分岐は、すべて finally を通るか確認します
- フラグを立てる前に判定できるチェックは、立てる前にやります
- 同じファイルでも、安全な書き方と危険な書き方は混在しえます。「隣が大丈夫だからここも大丈夫」とは限りません
このテーマの括り
後始末が必要な処理は「いつ後始末スコープに入るか」を意識して書くと、この種の固着が防げます。
おわりに
原因は「return を1行どこに置くか」だけ。passKey が両方空になる経路は動作確認では稀すぎて踏まないし、コードを眺めても流れが自然に見えて気づきにくいです。第三者レビュー(AI)が「ここは finally を通らない」と指摘して初めて表面化する類のバグです。
この記事は2026年5月時点の自分の理解で書いています。