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

こんにちは!フロントエンドからNode.jsの深部まで、日夜JavaScriptと向き合っているあなたの先輩エンジニアです。

今回は、JavaScriptのモダン開発において避けて通れない「非同期関数(`async/await`)とスコープ、そしてPromiseチェーンにおける変数の共有」についてお話しします。

「非同期処理の中で変数が消えてしまう」「awaitを挟んだら前のステップで作ったはずの変数が参照できない」——そんな壁にぶつかったことはありませんか?

ここをしっかりとクリアすれば、JavaScriptの非同期制御とスコープの仕組みはバッチリマスターできますよ。表面的な書き方の暗記ではなく、ランタイムの裏側で何が起きているのかという本質から、優しく紐解いていきましょう。

—

1. なぜ非同期処理では変数の共有でつまずくのか?

JavaScriptはシングルスレッドで動作する言語です。つまり、基本的には一度に一つのことしか処理できません。しかし、ネットワーク通信やファイルの読み書きといった「時間がかかる処理」を待っている間にブラウザがフリーズしては困りますよね。そこで登場するのが非同期処理です。

`async/await` は、その非同期処理をまるで同期処理(上から下へ順番に進む処理)のように美しく書くための魔法の構文です。

しかし、ここで初学者の多くが混乱するポイントがあります。それは、「処理が中断・再開する(`await`の前後)」ことで、変数の寿命やスコープがどう変化するのかという点です。

スコープの基本を復習しておきましょう

JavaScriptでは、変数の宣言に `let` や `const` を使います。これらは「ブロックレベルスコープ」を持ち、波括弧 `{}` の内側で宣言された変数は、その外側からは見えません。

非同期関数(`async function`)の中でもこれは全く同じですが、`await` キーワードを挟むことで、時間の流れが一度「一時停止」する点が非同期ならではの挙動を生み出します。

—

2. 【基本編】`async/await` における変数の引き継ぎ

まずは、基本的な `async/await` の中でどうやって変数を共有するのかを見てみましょう。

以下のコードを見てください。ユーザーIDからユーザー情報を取得し、さらにその所属部署のデータを取得するシナリオです。

// 模擬的なAPI通信を行う非同期関数
const fetchUser = async (userId) => {
return { id: userId, name: ‘Alice’, departmentId: ‘dept-88’ };
};

const fetchDepartment = async (deptId) => {
return { id: deptId, name: ‘開発部’, location: ‘東京’ };
};

// メインの処理を行う非同期関数
const processUserWorkflow = async (userId) => {
// ステップ1: ユーザー情報を取得
// ここで const で宣言した変数 ‘user’ は、この関数スコープ全体で生存します
const user = await fetchUser(userId);
console.log(`[ステップ1完了] ユーザー取得: ${user.name}`);

// ステップ2: ステップ1で取得した user のプロパティを使って次の非同期処理へ
// user 変数は直前の await の後でも問題なく参照できます
const department = await fetchDepartment(user.departmentId);
console.log(`[ステップ2完了] 部署取得: ${department.name}`);

// ステップ3: すべての変数をまとめて結果を返す
return {
userName: user.name,
deptName: department.name,
};
};

// 実行してみましょう
processUserWorkflow(1)
.then(result => {
console.log(‘ワークフロー最終結果:’, result);
})
.catch(error => {
console.error(‘エラーが発生しました:’, error);
});

ここがポイント!

`async` 関数の中では、`await` で処理が一旦停止(V8エンジンのコンテキストが一時停止)しても、その関数スコープ(実行コンテキスト)内で宣言された変数(上記の `user` など)はメモリ上に保持され続けます。そのため、次の行の `await` 以降でも何事もなかったかのようにその変数を利用できるのです。

—

3. 【応用編】複数のステップにまたがる変数のスコープ設計と罠

実務では、「後のステップで、前のステップのデータだけでなく、もっと前のステップのデータも一緒に使いたい」というケースが多々あります。

ここでやりがちな文法エラーや設計ミスを見てみましょう。

❌ やりがちなミス:ブロックスコープの罠

const badWorkflow = async () => {
if (true) {
// 块(ブロック)の中で変数を作ってしまう
const token = await getTokenFromAPI();
}

// エラー! ReferenceError: token is not defined
// token は if文の {} の外からは見えない(ブロックスコープのため)
const userData = await getUserData(token);
};

他の言語から来た方がやってしまいがちなのが、このように処理のブロックごとに `if` や `{}` で囲んでしまい、必要な変数が外から見えなくなるミスです。

⭕ 正しいアプローチ:上位スコープでの変数宣言とクロージャの活用

複数の非同期ステップをまたいでデータを保持し続けるには、「必要な変数は、それらを利用するスコープよりも上の階層で宣言する」のが鉄則です。

const smartWorkflow = async (userId) => {
// あらかじめ上位のスコープで変数を宣言しておく(初期値は null や undefined)
let user = null;
let permissions = null;
let auditLog = null;

try {
// ステップ1: ユーザー情報を取得して変数に代入
user = await fetchUser(userId);

// ステップ2: ユーザー権限を取得(user のデータが必要)
permissions = await fetchPermissions(user.id);

// ステップ3: 監査ログを記録(user と permissions の両方が必要!)
auditLog = await recordAudit({
userId: user.id,
role: permissions.role,
timestamp: Date.now()
});

return { success: true, auditLogId: auditLog.id };

} catch (err) {
// 万が一どこかのステップでエラーが起きた場合でも、
// どの段階まで処理が進んでいたか(user は取得できていたか等)をスコープ経由でデバッグに活かせる
console.error(`エラー発生時のユーザー情報:`, user ? user.name : ‘未取得’);
throw err;
}
};

このように、非同期関数のトップレベル(あるいは適切なクロージャの内側)で `let` を使って変数を宣言・保持しておけば、`await` の山を越えても、データのバケツリレーを綺麗に行うことができます。

—

4. クロージャを組み合わせて「状態」をカプセル化する

さらに高度なテクニックとして、非同期処理の途中で得た変数を外から直接書き換えられないように保護しつつ、特定の関数群だけで共有したい場合はクロージャ(Closure)が強力な武器になります。

// セッション管理を行う関数ファクトリー(クロージャの活用)
const createSessionManager = () => {
// この変数は外部から直接アクセスできない(カプセル化されている)
let sessionToken = null;
let currentUser = null;

return {
// ログイン処理(トークンを保持する)
login: async (credentials) => {
const response = await apiAuthenticate(credentials);
sessionToken = response.token;
currentUser = response.user;
console.log(`[ログイン成功] ${currentUser.name} のセッションを保持しました。`);
return currentUser;
},

// 保持しているトークンを使ってデータを取得する処理
fetchProfile: async () => {
if (!sessionToken) {
throw new Error(‘ログインしていません。’);
}
// 外から隠蔽された sessionToken を内部で利用する
return await apiGetProfile(sessionToken);
},

// セッションを破棄する
logout: async () => {
if (sessionToken) {
await apiRevokeToken(sessionToken);
}
sessionToken = null;
currentUser = null;
console.log(‘[ログアウト完了]’);
}
};
};

// 使い方
const session = createSessionManager();

// 非同期で実行
(async () => {
await session.login({ username: ‘bob’, password: ‘secretpassword’ });

// sessionToken を意識することなく、安全に次の非同期処理を呼べる
const profile = await session.fetchProfile();
console.log(‘プロフィール情報:’, profile);

await session.logout();
})();

このパターンでは、`sessionToken` や `currentUser` といった変数が `createSessionManager` のスコープ内に閉じ込められており、非同期のステップ(`login` -> `fetchProfile` -> `logout`)の間で安全に共有・維持されています。オブジェクト指向の「カプセル化」に近い概念を、JavaScriptのクロージャの仕組みを使ってエレガントに実現している好例です。

—

まとめ:今回の重要ポイント

  • `await` は一時停止するだけ:

`async` 関数内の変数は、`await` をまたいでも同じ関数スコープ内でしっかりと生存し続けます。

  • ブロックスコープに注意する:

`if` 文や不要な `{}` の中で `const` 宣言してしまうと、外のステップで参照できなくなるため、必要な変数は適切な上位スコープで `let` 宣言する。

  • クロージャで状態を守る:

複数の非同期関数間でデータを安全に共有・管理したいときは、クロージャを使って変数を隠蔽・カプセル化する設計を取り入れると非常にスマートになる。

非同期処理とスコープの仕組みが頭の中でクリアに繋がると、JavaScriptを書くのが一気に楽しく、そして自信が持てるようになりますよ。

ここをクリアしたあなたなら、もう中級者の扉を確実に叩いています。ぜひ実際のコードでも試してみてくださいね!それでは、また次の知見でお会いしましょう。

タイトルとURLをコピーしました