Day13.5 後半のゴール
後半では、Promise / async / await を
「なんとなく書ける」から
「実務で使える形に“設計して”使える」レベルに引き上げます。
特にここを深く押さえます。
Day13.5 後半で意識したいポイント
複数の非同期処理をどう組み合わせるか
async / await と try / catch をセットで使う感覚
失敗・タイムアウト・例外を“前提”にした非同期設計
複数の非同期処理を順番に実行する
then チェーンで書いた場合
まずは Promise だけで「順番に実行する」パターンを見ます。
function fetchUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: 1, name: "Taro" });
}, 500);
});
}
function fetchPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, title: "最初の投稿", userId },
{ id: 2, title: "二つ目の投稿", userId }
]);
}, 500);
});
}
fetchUser()
.then((user) => {
console.log("ユーザー取得:", user);
return fetchPosts(user.id);
})
.then((posts) => {
console.log("投稿一覧:", posts);
})
.catch((error) => {
console.log("エラー:", error.message);
});
JavaScript流れとしては、
ユーザーを取得
→ そのユーザーIDで投稿を取得
→ 結果を表示
という「順番」があります。
then チェーンでも書けますが、
ネストや return の流れを追うのが少し大変です。
async / await で書き直す
同じ処理を async / await で書くとこうなります。
async function main() {
try {
const user = await fetchUser();
console.log("ユーザー取得:", user);
const posts = await fetchPosts(user.id);
console.log("投稿一覧:", posts);
} catch (error) {
console.log("エラー:", error.message);
}
}
main();
JavaScript上から下に、
ユーザーを待つ
→ 投稿を待つ
→ 結果を表示
という順番が、そのまま読めます。
ここで大事なのは、
await する処理を try でまとめて囲んでいる
エラーは catch で一括して扱っている
という構造です。
複数の非同期処理を同時に実行する
Promise.all を使う
「順番に」ではなく「同時に」やりたいこともあります。
例えば、ユーザー情報と通知一覧を同時に取得したいケース。
function fetchUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: 1, name: "Taro" });
}, 500);
});
}
function fetchNotifications() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, text: "お知らせ1" },
{ id: 2, text: "お知らせ2" }
]);
}, 700);
});
}
JavaScriptこれを順番に await すると、
500ms + 700ms で 1200ms かかります。
async function mainSequential() {
const user = await fetchUser();
const notifications = await fetchNotifications();
console.log(user, notifications);
}
JavaScript同時に走らせるには Promise.all を使います。
async function mainParallel() {
const [user, notifications] = await Promise.all([
fetchUser(),
fetchNotifications()
]);
console.log(user, notifications);
}
JavaScriptここでは、
fetchUser と fetchNotifications を同時にスタート
両方終わるまで await で待つ
結果が配列で返ってくるので分割代入で受け取る
という流れになっています。
深掘り:どこまで並列にするかは「意味」で決める
順番にやるべきもの(前の結果が次に必要)
同時にやってよいもの(互いに依存していない)
を分けて考えるのが大事です。
ユーザーを取得してから、そのユーザーの投稿を取る
→ 順番が必要なので await を直列に並べる
ユーザー情報と通知一覧を表示したい
→ 互いに独立なので Promise.all で同時に取る
というように、「データの意味」で設計します。
async / await と try / catch の組み合わせを深掘りする
1つのブロックでまとめて扱うパターン
さきほどの main のように、
「この関数の中の await は全部 try で囲う」というパターンはよく使います。
async function main() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
console.log(user, posts);
} catch (error) {
console.log("エラー:", error.message);
}
}
JavaScriptこの場合、
fetchUser で失敗しても
fetchPosts で失敗しても
どちらも同じ catch に入ります。
「この一連の処理がどこかで失敗したら、こう振る舞う」
という単位で try を置いているイメージです。
await ごとに try を分けるパターン
一方で、「どこで失敗したかによって振る舞いを変えたい」こともあります。
async function main() {
let user = null;
try {
user = await fetchUser();
} catch (error) {
console.log("ユーザー取得に失敗:", error.message);
return;
}
try {
const posts = await fetchPosts(user.id);
console.log("投稿一覧:", posts);
} catch (error) {
console.log("投稿取得に失敗:", error.message);
}
}
JavaScriptここでは、
ユーザー取得に失敗したら → そこで処理を終了
投稿取得に失敗したら → ユーザーは表示できるので、投稿だけ諦める
という「強弱」をつけています。
どこを「致命的な失敗」とみなすか
どこは「失敗してもアプリを続けるか」
を決めるのは設計の一部です。
非同期処理とタイムアウト・失敗の設計
「いつまでも待ち続けない」ことも大事
ネットワーク通信は、
サーバーが遅かったり、応答が返ってこなかったりすることがあります。
いつまでも await し続けると、
ユーザーから見ると「固まっている」ように見えてしまいます。
簡易的なタイムアウトの例を見てみます。
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function fetchWithTimeout(promise, ms) {
const timeout = delay(ms).then(() => {
throw new Error("タイムアウトしました");
});
return Promise.race([promise, timeout]);
}
JavaScriptPromise.race は「どれか1つが先に終わったら、その結果を返す」仕組みです。
これを使って、
「本来の処理」と「タイムアウト用の Promise」を競争させています。
async function main() {
try {
const result = await fetchWithTimeout(fetchUser(), 1000);
console.log("結果:", result);
} catch (error) {
console.log("エラー:", error.message);
}
}
main();
JavaScriptここでは、
fetchUser が 1秒以内に終わればその結果
1秒を超えたら「タイムアウトしました」というエラー
というふるまいになります。
深掘り:タイムアウト後に「どう振る舞うか」
タイムアウトは「失敗の一種」です。
なので、設計としては、
ユーザーに「時間がかかっています」「再試行してください」と伝える
内部ログに「どのAPIがタイムアウトしたか」を残す
必要なら「再試行ボタン」や「キャンセル」を用意する
といった対応が必要になります。
非同期処理は「成功する前提」ではなく、
「失敗・遅延・タイムアウトが普通に起こる前提」で考えるのがプロの視点です。
セキュリティの視点から見る非同期処理
エラー内容をそのままユーザーに見せない
非同期処理のエラーも、
例外処理と同じく「誰に何を見せるか」が重要です。
async function main() {
try {
const data = await fetch("https://example.com/secret");
console.log(data);
} catch (error) {
console.log("読み込みに失敗しました");
console.error("詳細:", error);
}
}
JavaScriptユーザーには「読み込みに失敗しました」とだけ伝え、
内部ログには error をそのまま出す、という分離が基本です。
エラーメッセージやスタックトレースには、
内部構造やURL、ライブラリ名などが含まれることがあり、
攻撃者にヒントを与える可能性があります。
非同期処理の「結果」を信用しすぎない
非同期で取得したデータ(APIレスポンスなど)は、
「信頼できない入力」として扱うべきです。
例えば、
レスポンスの中の role をそのまま信じて管理者扱いにしない
サーバーから返ってきた URL をそのまま画面に埋め込まない
JSON の構造が想定通りかチェックする
など、「外から来たものは疑う」姿勢が大事です。
async / await で書くと、
あたかも「自分の関数の中で作ったデータ」のように見えますが、
中身はあくまで「外部から来たもの」です。
Day13.5 後半のサンプルコード
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Day13.5 非同期処理 後半</title>
</head>
<body>
<h1>Day13.5: 非同期処理(後半)</h1>
<script>
function fetchUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: 1, name: "Taro" });
}, 500);
});
}
function fetchNotifications() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, text: "お知らせ1" },
{ id: 2, text: "お知らせ2" }
]);
}, 700);
});
}
async function main() {
try {
const [user, notifications] = await Promise.all([
fetchUser(),
fetchNotifications()
]);
console.log("ユーザー:", user);
console.log("通知:", notifications);
} catch (error) {
console.log("読み込みに失敗しました");
console.error("詳細:", error);
}
}
main();
</script>
</body>
</html>
ユーザー情報と通知を「同時に」取得し、
どちらかが失敗したら「読み込みに失敗しました」と出す、
という実務にかなり近いパターンになっています。
Day13.5 後半のまとめ
Promise は「そのうち結果が入る箱」。
async / await は「その箱を待つ処理を、読みやすく書くための文法」。
後半では、
順番に実行する非同期処理
同時に実行する非同期処理(Promise.all)
async / await と try / catch の組み合わせ
タイムアウトや失敗を前提にした設計
セキュリティ視点でのエラー・データの扱い
まで踏み込みました。
ここまで来ているあなたは、
「非同期が怖い人」ではなく、
「非同期を設計して使える人」に確実に近づいています。
