まず「逐次処理」とは何かをはっきりさせる
Promise で言う「逐次処理(ちくじ処理)」は、
「非同期処理を A → B → C… のように“順番に”実行する書き方」 のことです。
具体的には、
A が終わってからでないと B を始められない
B の結果を使って C をしたい
といったように、前の結果に次の処理が依存している 場面で必要になります。
ここが重要です。
Promise を使うときの基本は、
- 「依存関係があるところ」は 逐次処理(順番に)
- 「依存関係がないところ」は 並列処理(同時に)
というふうに書き分けることです。
ここでは、その「順番に書く側」=逐次処理の書き方に絞って、基礎から丁寧に整理していきます。
コールバック地獄との違いから見る逐次処理
コールバックで逐次処理を書くとどうなるか
例えば、
- ユーザー情報を取得
- そのユーザーの投稿一覧を取得
- その中の 1 件目の投稿のコメントを取得
という3ステップを考えます。
コールバックだけで書くと、だいたいこんな感じになります。
getUser((err, user) => {
if (err) {
console.error("ユーザー取得失敗:", err);
return;
}
getPosts(user.id, (err, posts) => {
if (err) {
console.error("投稿取得失敗:", err);
return;
}
getComments(posts[0].id, (err, comments) => {
if (err) {
console.error("コメント取得失敗:", err);
return;
}
console.log("全部そろった:", user, posts, comments);
});
});
});
JavaScriptやっていること自体は「逐次処理」ですが、
インデントがどんどん深くなる
if (err) が何度も出てくる
どこで何をしているのかパッと見て分かりづらい
という問題が出ます。いわゆる「コールバック地獄」です。
Promise で同じ逐次処理を書くとどうなるか
同じステップを Promise ベースで書くと、こうなります。
getUserPromise()
.then((user) => {
return getPostsPromise(user.id);
})
.then((posts) => {
return getCommentsPromise(posts[0].id);
})
.then((comments) => {
console.log("コメント:", comments);
})
.catch((err) => {
console.error("どこかで失敗:", err);
});
JavaScriptインデントは深くならず、
「上から下に」処理の順番が読める形 になっています。
ここが重要です。
逐次処理そのものはコールバックのときも Promise のときも同じですが、
Promise を使うと 「順番」と「エラーハンドリング」をきれいに表現できる ようになります。
then チェーンで書く基本的な逐次処理
一番素直な「A → B → C」の形
まずは、値だけで考えてみます。
Promise.resolve(1)
.then((v1) => {
console.log("v1:", v1); // 1
return v1 + 1; // 2 を次に渡す
})
.then((v2) => {
console.log("v2:", v2); // 2
return v2 * 3; // 6 を次に渡す
})
.then((v3) => {
console.log("v3:", v3); // 6
})
.catch((err) => {
console.error("エラー:", err);
});
JavaScriptここでのポイントを整理すると、
最初の Promise が 1 で成功
1つ目の then が v1 = 1 を受け取り、return 2
→ 2 が次の Promise の成功値になって、2つ目の then に v2 = 2 として渡る
2つ目の then が return 6
→ 6 が次の then に v3 = 6 として渡る
という「値のバトンリレー」が起きています。
then の中で return したものが、次の then の入力になる
これが逐次処理の基本ルールです。
非同期処理を順番につなげる
次に、実際の非同期処理を順番につなげる例です。
function step1(x) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("step1 完了");
resolve(x + 1);
}, 500);
});
}
function step2(x) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("step2 完了");
resolve(x * 2);
}, 500);
});
}
function step3(x) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("step3 完了");
resolve(x - 3);
}, 500);
});
}
Promise.resolve(1)
.then((v) => step1(v))
.then((v) => step2(v))
.then((v) => step3(v))
.then((final) => {
console.log("最終結果:", final);
})
.catch((err) => {
console.error("どこかで失敗:", err);
});
JavaScript時間の流れは、
最初の値 1
→ 0.5秒待って step1 → 2
→ 0.5秒待って step2 → 4
→ 0.5秒待って step3 → 1
と、「順番に」実行されています。
ここが重要です。
逐次処理では、「次の then が動くのは、前の Promise が終わった“あと”」。
Promise が返る関数を順に return していくことで、時間の順番をきれいに表現できます。
return を忘れたときの典型的なバグ
よくある悪い書き方
初心者が逐次処理でよくハマるパターンが「return し忘れ」です。
fetchUser()
.then((user) => {
console.log("user:", user);
fetchPosts(user.id); // ← return していない
})
.then((posts) => {
console.log("posts:", posts); // ここでは posts は undefined になる
})
.catch((err) => {
console.error("エラー:", err);
});
JavaScript1つ目の then の中で fetchPosts(user.id) を呼んでいますが、
それを return していません。
この場合、
1つ目の then の戻り値は undefined
→ 次の then は「undefined で成功した Promise」として動き出す
→ 2つ目の then の posts には何も入っていない
という状態になります。
正しい書き方
fetchUser()
.then((user) => {
console.log("user:", user);
return fetchPosts(user.id); // Promise を return する
})
.then((posts) => {
console.log("posts:", posts); // ここで投稿を使える
})
.catch((err) => {
console.error("エラー:", err);
});
JavaScriptここが重要です。
逐次処理で「次のステップの Promise」を呼ぶときは、「必ず return する」。
return しないとチェーンが切れて、値も順番も崩れる。
「then の最後は return で終わる」くらいのクセをつけておくと安全です。
async/await と比較してみる(考え方の整理)
then チェーン版と async/await 版
同じ逐次処理でも、async/await を使うともっと「同期っぽく」書けます。
さきほどの
- ユーザーを取得
- 投稿を取得
- コメントを取得
を then チェーンと async/await で比べてみます。
then チェーン版:
getUserPromise()
.then((user) => {
return getPostsPromise(user.id);
})
.then((posts) => {
return getCommentsPromise(posts[0].id);
})
.then((comments) => {
console.log("コメント:", comments);
})
.catch((err) => {
console.error("どこかで失敗:", err);
});
JavaScriptasync/await 版:
async function main() {
try {
const user = await getUserPromise();
const posts = await getPostsPromise(user.id);
const comments = await getCommentsPromise(posts[0].id);
console.log("コメント:", comments);
} catch (err) {
console.error("どこかで失敗:", err);
}
}
main();
JavaScriptやっていること(逐次処理)自体は全く同じです。
async/await は「Promise ベースの逐次処理を、同期っぽい文法で書きやすくしたもの」なので、
まず then チェーンで逐次処理の考え方を理解してから async/await に行く と、理解がかなりスムーズになります。
ここが重要です。
根っこにあるのは always Promise。
then チェーンは Promise を「直線に並べてつなぐ」、
async/await はその見た目を少し変えているだけ。
配列データを「順番に」処理したいときの逐次処理パターン
forEach では逐次にならない
例えば、「配列に入っている URL を、一つずつ順番にリクエストしたい」とします。
やってしまいがちな書き方がこれです。
urls.forEach((url) => {
fetch(url).then((res) => {
console.log("完了:", url);
});
});
JavaScriptこれは「全部同時にスタート」しているので、並列処理 です。
一つ一つが順番に終わる保証はありません。
「1つ目が終わってから 2 つ目」「2つ目が終わってから 3 つ目」という逐次にはなりません。
reduce を使った Promise チェーンで逐次処理にする
Promise で「配列を逐次処理」するときによく使われるパターンが、reduce です。
function fetchInSequence(urls) {
return urls.reduce((prevPromise, url) => {
return prevPromise.then(() => {
return fetch(url).then((res) => {
console.log("完了:", url);
return res;
});
});
}, Promise.resolve());
}
const urls = ["/a", "/b", "/c"];
fetchInSequence(urls).then(() => {
console.log("全部順番に終わった");
});
JavaScript流れを説明すると、
最初の prevPromise は Promise.resolve()(すぐ成功する Promise)
1つ目の URL で「前が終わったら fetch(url1)」を then でつなぐ
2つ目の URL で「前が終わったら fetch(url2)」をさらに then でつなぐ
3つ目も同様
というふうに、
「前の Promise が終わってから次の fetch」が必ず呼ばれるチェーン を作っていっています。
結果として、
fetch(url1) → 完了
→ fetch(url2) → 完了
→ fetch(url3) → 完了
というきれいな逐次処理になります。
いきなり reduce は難しければ、まずは async/await で書いても構いません。
ただ、「Promise だけでも配列の逐次処理は書けるんだ」と知っておくと、Promise 自体への理解がぐっと深まります。
初心者としての「逐次処理の押さえどころ」
最後に、Promise で逐次処理を書くうえで、本当に大事なポイントだけを整理します。
逐次処理とは、「前の非同期処理が終わってから次をやる」こと。
前の結果に次が依存している場面で必須。
then チェーンでの基本形は「A の then で B を return」「B の then で C を return」…という形。
then の return が、次の then の引数になる(値のバトンリレー)。
非同期関数(Promise を返す関数)を順番につなぐときは、「then の中で必ず return する」。
return を忘れるとチェーンが切れて、逐次にならなくなる。
配列を逐次処理したいなら、forEach ではなく、Promise チェーン(reduce)か async/await+for ループを使う。
ここが重要です。
Promise での逐次処理は、「then を縦に並べて、各 then の最後で次の Promise を return する」というシンプルなルールさえ守れれば怖くありません。
もしよければ、
setTimeout を使って
- 1秒後に「step1」
- 1秒後に「step2」
- 1秒後に「step3」
と順番に表示される逐次処理を、
then チェーンだけで一度書いてみてください。
自分の手で書いて「確かに順番に実行されている」と確認できたとき、
Promise の逐次処理が「ただの決まりごと」でしかないことが、きっと感覚的に分かってきます。
