【実務・中級編】非同期関数(async/await)とスコープ:Promiseチェーンにおける変数の共有 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

非同期処理のスコープ崩壊を防げ:Promiseチェーンと`async/await`における変数共有の極意

コードレビューをしていて、最も多く遭遇するアンチパターンの一つが「非同期処理をまたいだ変数のスコープ管理の失敗」だ。

「APIからユーザーデータを取得し、そのIDを使って関連リソースを取得し、最後にUIを更新する」といった一連のシーケンシャルな処理の中で、`let`で宣言された変数がブロックのあちこちで書き換えられ、どの非同期ステップでどの値を持っているのか誰にも追えなくなっているコードを見たことはないだろうか。

JavaScriptの非同期処理(Promiseチェーンや`async/await`)は、V8エンジンのイベントループ上でタスクを非同期に実行する。しかし、言語のスコープ(レキシカルスコープとクロージャ)のルールは同期的なコードと何ら変わらない。この非同期性とスコープの乖離を理解していないと、競合状態(Race Condition)や意図しない変数の上書き、さらにはメモリリークの温床を作り出すことになる。

今回は、プロダクション環境で耐えうる、非同期処理における堅牢な変数共有とスコープ設計の極意を、チーフアーキテクトの視点から授けよう。

—

1. なぜ「`let`の使い回し」はバグの温床になるのか

まずは、よくある愚かなコードを見てほしい。非同期処理の各ステップで使い回すために、関数の外側や上位スコープで`let`変数を宣言してしまうパターンだ。

// 【アンチパターン】上位スコープのlet変数を非同期でミューテーションする例
async function processUserSessionBad(userId) {
let userData = null;
let permissions = null;
let auditLog = null;

try {
userData = await fetchUserData(userId);
// ここで userData に依存した別の非同期処理
permissions = await fetchPermissions(userData.roleId);

// さらに別の処理…
auditLog = await postAuditTrail({ userId, action: ‘LOGIN’ });

} catch (error) {
console.error(‘Failed’, error);
}

// 最終的にこれらの変数を返す
return { userData, permissions, auditLog };
}

このコードの何が問題か。
1. 可変性(Mutability)の蔓延: 変数が常に書き換え可能(`let`)であるため、コードのどこで値が変更されたかを追うためにメンタルモデルの負荷が高すぎる。
2. スコープの汚染: 本来必要のない生存期間(Lifecycle)まで変数が生き残り、V8のガベージコレクタ(GC)によるメモリ回収の効率を落とす(特に巨大なオブジェクトや配列を保持している場合)。
3. 並行処理への耐性のなさ: もしこの関数が短時間に複数回呼び出された場合、クロージャやスコープの閉じ込めが甘いと、予期せぬ変数の混線を生む原因になる。

—

2. プロダクションコードにおける「イミュータブル・パイプライン」設計

では、どう設計すべきか。答えは明快だ。「変数を書き換えるな、スコープを閉じ込めろ、そしてデータをバトンリレーのように渡せ」。

`async/await`の本質は、見かけ上の同期的記述であって、内部はPromiseの連鎖である。したがって、関数型プログラミングのパイプラインの思想を取り入れ、各非同期ステップの結果を不変(`const`)なスコープに閉じ込めて次のステップへ渡していくのが最も堅牢である。

以下のプロダクションコードを見てほしい。これは、フロントエンドの複雑な非同期API連携とDOM/状態管理を想定した、保守性の極めて高いモジュールの実装例だ。

/