JavaScript .then() の使い方徹底解説【Promise入門】

はい、承知いたしました。「JavaScript .then() の使い方徹底解説【Promise入門】」に関する、約5000語の詳細な解説記事を作成します。


JavaScript .then() の使い方徹底解説【Promise入門】

はじめに:非同期処理の夜明けとPromiseという光明

現代のWebアプリケーション開発において、JavaScriptはもはや単なる「動きをつけるため」の言語ではありません。バックエンドとの通信、ファイルの読み込み、複雑な計算など、ブラウザやNode.js環境で様々なタスクを実行します。これらのタスクの多くは、完了までに時間がかかる可能性があり、その間にプログラムの実行をブロックしてしまうと、ユーザー体験が著しく損なわれてしまいます。

例えば、Webサイトでサーバーからデータを取得するAPI呼び出しを考えてみましょう。もしこの処理が同期的に行われると、データが到着するまでブラウザは何もできなくなり、画面がフリーズしたように見えます。ユーザーはボタンをクリックしても反応がなく、イライラしてサイトを離れてしまうかもしれません。

このような問題を解決するために、JavaScriptでは「非同期処理」という概念が不可欠です。非同期処理を利用することで、時間がかかるタスクを実行している間も、プログラムの他の部分(例えばUIの更新)は引き続き実行され、アプリケーションは応答性を保つことができます。

かつて、JavaScriptで非同期処理を扱う主要な方法は「コールバック関数」でした。処理が終わったときに実行してほしい関数を、非同期関数に引数として渡すのです。

javascript
// コールバックを使った非同期処理の例(架空の関数)
getDataFromServer(url, function(error, data) {
if (error) {
console.error("データの取得に失敗しました:", error);
} else {
console.log("データ:", data);
// 取得したデータを使って次の処理...
processData(data, function(error2, result) {
if (error2) {
console.error("データの処理に失敗しました:", error2);
} else {
console.log("処理結果:", result);
// さらに次の処理...
saveResult(result, function(error3) {
if (error3) {
console.error("結果の保存に失敗しました:", error3);
} else {
console.log("結果を保存しました");
}
});
}
});
}
});

一見シンプルに見えますが、非同期処理が複数連なったり、条件分岐やエラーハンドリングが加わると、コールバック関数がネストされ、コードの階層がどんどん深くなっていきます。これを「Callback Hell(コールバック地獄)」と呼びます。Callback Hellに陥ったコードは、可読性が著しく低下し、デバッグや保守が非常に困難になります。

Callback Hellの課題:
* 可読性の低下: コードが横に広がっていき、処理の流れを追いにくい。
* エラーハンドリングの複雑さ: 各コールバック内で個別にエラーをチェックする必要があり、一元的なエラー処理が難しい。
* 制御フローの把握困難: 同期的なコードとは異なる制御フローになり、直感的に理解しにくい。

このような背景から、非同期処理をより扱いやすく、読みやすいコードで記述するための新しい仕組みが求められるようになりました。そこで登場したのが「Promise」です。

Promiseは、非同期処理の「未来の結果」を表すオブジェクトです。処理が成功した場合はその「値」を、失敗した場合はその「理由(エラー)」を持つことを約束(Promise)します。Promiseを使うことで、非同期処理の完了や失敗を待って、後続の処理を連鎖的に定義できるようになります。

そして、このPromiseの力を引き出すために不可欠なメソッドが、今回主役となる.then()です。.then()を使うことで、「Promiseが成功したら〇〇をする」「Promiseが失敗したら△△をする」という処理を、コールバック地獄に陥ることなく、すっきりとしたコードで記述できるようになります。

この記事では、JavaScriptのPromiseがなぜ必要なのかという導入から始め、Promiseの基本的な構造を理解し、そして最も重要なメソッドである.then()の使い方を徹底的に解説します。Promiseチェーンによる連続した非同期処理の記述方法、適切なエラーハンドリング、そして関連するメソッドやasync/awaitとの関係性まで、Promiseを使った非同期処理マスターへの道を切り開きます。約5000語のボリュームで、Promiseと.then()に関するあなたの疑問を解消し、実践的な応用力が身につくことを目指します。

さあ、Promiseと.then()の世界へ飛び込みましょう!

非同期処理とは?なぜPromiseが必要なのか?(深掘り)

Promiseや.then()の理解を深める前に、JavaScriptにおける非同期処理の仕組みとその重要性をもう少し掘り下げてみましょう。

同期処理と非同期処理

  • 同期処理: コードが書かれた順番に、一つずつ実行されます。ある処理が完了するまで、次の処理は待機します。
    javascript
    console.log("処理1");
    // 時間のかかる同期処理(実際にはあまり使わないがイメージとして)
    const startTime = Date.now();
    while (Date.now() - startTime < 2000) {
    // 2秒間待機(ブラウザやNode.jsがフリーズする!)
    }
    console.log("処理2");
    console.log("処理3");
    // 出力:
    // 処理1
    // (2秒後に)
    // 処理2
    // 処理3

    この例のように、時間のかかる処理があると、その間プログラムは完全に停止します。GUIアプリケーションであれば画面が固まり、サーバーサイドであれば他のリクエストを処理できなくなります。

  • 非同期処理: 時間のかかる可能性のある処理を、プログラムのメイン実行フロー(イベントループ)から切り離して実行します。非同期処理を開始した後、プログラムは次の行のコードの実行に移ります。非同期処理が完了すると、あらかじめ指定しておいたコールバック関数などが呼び出され、その結果を処理します。
    javascript
    console.log("処理1");
    // 非同期処理(例: 2秒後に実行されるタイマー)
    setTimeout(() => {
    console.log("非同期処理完了 (処理2)");
    }, 2000);
    console.log("処理3");
    // 出力:
    // 処理1
    // 処理3
    // (2秒後に)
    // 非同期処理完了 (処理2)

    この例では、setTimeoutが開始された後、その完了を待たずにすぐにconsole.log("処理3")が実行されていることがわかります。非同期処理はバックグラウンドで行われ、完了したときに登録しておいた関数(ここではアロー関数)が実行されます。

JavaScriptのシングルスレッドモデルと非同期処理の必要性

JavaScriptの実行環境(ブラウザのJavaScriptエンジンやNode.js)の多くは、基本的に「シングルスレッド」です。これは、一度に一つのタスクしか実行できないということです。もし時間のかかるI/O処理(ネットワーク通信、ファイルアクセスなど)を同期的に行ってしまうと、そのI/Oが完了するまでスレッドがブロックされ、他の全ての処理が停止してしまいます。

これを避けるために、JavaScriptは非同期API(setTimeout, setInterval, fetch, XMLHttpRequest, Node.jsのファイルシステム操作など)を提供しています。これらのAPIを呼び出すと、JavaScriptエンジンはOSやブラウザの機能にタスクを依頼し、自分自身はブロックされずに次の処理に進みます。依頼したタスクが完了すると、OSやブラウザがJavaScriptの実行環境に通知し、それに応じて登録されていたコールバック関数などが「イベントループ」によって実行されます。

つまり、非同期処理はJavaScriptのシングルスレッドという制約の中で、アプリケーションの応答性や効率性を保つための必須の仕組みなのです。

コールバック関数による非同期処理の課題(Callback Hell再考)

非同期処理を扱う初期の方法としてコールバック関数が広く使われていました。

javascript
// 例:ユーザーデータの取得 -> 投稿リストの取得 -> 投稿詳細の取得
getUser(userId, function(user) {
getPosts(user.id, function(posts) {
getPostDetail(posts[0].id, function(postDetail) {
console.log("ユーザーと最初の投稿の詳細:", user, postDetail);
});
});
});

これはまだ単純な例ですが、実際にはエラーハンドリング、条件分岐(もし投稿がなかったら?)、他の非同期処理との組み合わせなどが加わり、コードのネストはすぐに深くなります。

javascript
getUser(userId, function(err, user) {
if (err) {
handleError(err);
return;
}
getPosts(user.id, function(err, posts) {
if (err) {
handleError(err);
return;
}
if (posts.length === 0) {
console.log("投稿がありません");
return;
}
getPostDetail(posts[0].id, function(err, postDetail) {
if (err) {
handleError(err);
return;
}
console.log("ユーザーと最初の投稿の詳細:", user, postDetail);
});
});
});

このように、エラーハンドリングを丁寧に行おうとすると、コードはさらに複雑になります。各レベルでエラーチェックが必要になり、エラーがどの非同期処理で発生したのかを追跡するのが難しくなります。これがCallback Hellと呼ばれる状態であり、非同期処理の記述と管理における大きな課題でした。

Promiseは、このCallback Hellから開発者を救済するために生まれました。Promiseは非同期処理の状態を標準化し、その結果(成功または失敗)を後続の処理に伝えるための統一的なインターフェースを提供します。そして、そのインターフェースの要となるのが、これから詳しく解説する.then()メソッドなのです。

Promiseの基本構造

Promiseについて学ぶ前に、その基本的な構造と状態遷移を理解することが重要です。

Promiseは、非同期操作の最終的な完了(または失敗)とその結果の値を表すオブジェクトです。Promiseオブジェクトは、以下の3つの状態のいずれかをとります。

  1. Pending(ペンディング): 非同期操作が進行中の初期状態です。まだ成功も失敗もしていません。
  2. Fulfilled(フルフィルド): 非同期操作が成功裏に完了した状態です。この状態になると、成功した「値」が確定します。この状態は「Settled(セトルド)」状態の一つです。
  3. Rejected(リジェクテッド): 非同期操作がエラーによって失敗した状態です。この状態になると、失敗した「理由(エラー)」が確定します。この状態も「Settled(セトルド)」状態の一つです。

一度Pending以外の状態(Fulfilled または Rejected)になると、Promiseの状態はそれ以降変化しません。これを「Settled(決定済み)」な状態と呼びます。Settledな状態になったPromiseは、その結果(値または理由)を永続的に保持します。

Promiseオブジェクトは、通常、new Promise()コンストラクタを使って作成されます。このコンストラクタには、「executor(実行関数)」と呼ばれる関数を引数として渡します。

“`javascript
const myFirstPromise = new Promise((resolve, reject) => {
// ここに非同期処理を書く
// 処理が成功したら resolve(成功した値) を呼び出す
// 処理が失敗したら reject(失敗した理由) を呼び出す

setTimeout(() => {
const success = true; // 非同期処理の結果を模倣

if (success) {
  resolve("非同期処理が成功しました!"); // 成功を通知し、値を渡す
} else {
  reject("非同期処理が失敗しました..."); // 失敗を通知し、理由を渡す
}

}, 1000); // 1秒後に処理完了を模倣
});
“`

executor関数は、JavaScriptによって即座に実行されます。この関数は、JavaScriptエンジンによって2つの引数を受け取ります。慣習的にそれぞれresolverejectという名前にします。

  • resolve(value): 非同期処理が成功した場合に呼び出す関数です。Promiseの状態をPendingからFulfilledに変更し、引数として渡したvalueをPromiseの最終的な値として確定させます。
  • reject(reason): 非同期処理が失敗した場合に呼び出す関数です。Promiseの状態をPendingからRejectedに変更し、引数として渡したreason(通常はErrorオブジェクト)をPromiseの最終的な理由として確定させます。

重要な点として、resolveまたはrejectは一度しか呼び出されません。複数回呼び出しても、最初に呼び出されたものが優先され、Promiseの状態は一度だけ決定(Settled)されます。

これで、Promiseオブジェクトがどのように作成され、状態が変化するかの基本が分かりました。しかし、作成されたPromiseが成功したのか、それとも失敗したのか、そしてその結果の値や理由は何なのかを、プログラムの他の部分で知る必要があります。そのために使用するのが、次のセクションで詳しく解説する.then()メソッドです。

.then() メソッドの徹底解説

.then()メソッドは、Promiseにおいて非同期処理の「結果を待ち、それに応じた処理を実行する」ための最も基本的な、そして最も重要なメソッドです。

Promiseオブジェクトには、Pending状態からSettled状態(FulfilledまたはRejected)への変化を監視し、その状態に応じて登録されたコールバック関数を実行する機能があります。この機能を提供するのが.then()メソッドです。

.then()メソッドは、Promiseオブジェクトに対して呼び出します。その最も一般的な形式は、2つのコールバック関数を引数として受け取るものです。

javascript
promise.then(onFulfilled, onRejected);

  • onFulfilled: PromiseがFulfilled(成功)状態になったときに実行されるコールバック関数です。Promiseが成功した「値」を引数として受け取ります。
  • onRejected: PromiseがRejected(失敗)状態になったときに実行されるコールバック関数です。Promiseが失敗した「理由(エラー)」を引数として受け取ります。

これらのコールバック関数は、PromiseがSettled状態になった後に、非同期的に実行されます。これは、.then()メソッドを呼び出した時点ではPromiseがまだPending状態である可能性があるためです。PromiseがすでにSettled状態であったとしても、.then()に登録されたコールバックは現在の実行コンテキストが完了した後、マイクロタスクキューを通じて非同期的に実行されることに注意してください。

具体的なコード例:成功時の処理

先ほど作成したmyFirstPromiseを使って、成功時の処理を記述してみましょう。

“`javascript
const myFirstPromise = new Promise((resolve, reject) => {
// 1秒後に成功することをシミュレーション
setTimeout(() => {
resolve(“非同期処理が成功しました!”);
}, 1000);
});

console.log(“Promiseを開始しました…”);

myFirstPromise.then(
// onFulfilled コールバック
(successMessage) => {
console.log(“.then() – 成功:”, successMessage); // “非同期処理が成功しました!” が表示される
},
// onRejected コールバック(今回は失敗しないので実行されない)
(errorMessage) => {
console.error(“.then() – 失敗:”, errorMessage);
}
);

console.log(“.then() を登録しました。Promiseの完了を待っています…”);

// 実行結果の例:
// Promiseを開始しました…
// .then() を登録しました。Promiseの完了を待っています…
// (1秒後に)
// .then() – 成功: 非同期処理が成功しました!
“`

この例では、myFirstPromiseは1秒後に成功します。.then()メソッドの第1引数に渡したonFulfilledコールバック関数が、Promiseが成功した値である文字列 "非同期処理が成功しました!" を引数に受け取って実行されます。.then()を呼び出した時点ではPromiseはまだPending状態ですが、1秒後にFulfilledになったときにコールバックが実行されるのが分かります。

具体的なコード例:失敗時の処理

今度は、Promiseが失敗する場合の処理を記述してみましょう。

“`javascript
const mySecondPromise = new Promise((resolve, reject) => {
// 1秒後に失敗することをシミュレーション
setTimeout(() => {
reject(new Error(“非同期処理が失敗しました…”)); // Errorオブジェクトを理由として渡すのが一般的
}, 1000);
});

console.log(“別のPromiseを開始しました…”);

mySecondPromise.then(
// onFulfilled コールバック(今回は成功しないので実行されない)
(successMessage) => {
console.log(“.then() – 成功:”, successMessage);
},
// onRejected コールバック
(errorMessage) => {
console.error(“.then() – 失敗:”, errorMessage.message); // Errorオブジェクトのメッセージを表示
}
);

console.log(“.then() を登録しました。Promiseの完了を待っています…”);

// 実行結果の例:
// 別のPromiseを開始しました…
// .then() を登録しました。Promiseの完了を待っています…
// (1秒後に)
// .then() – 失敗: 非同期処理が失敗しました…
“`

この例では、mySecondPromiseは1秒後に失敗します。.then()メソッドの第2引数に渡したonRejectedコールバック関数が、Promiseが失敗した理由であるErrorオブジェクトを引数に受け取って実行されます。

引数の省略について

.then()メソッドの引数は必須ではありません。成功時だけ、または失敗時だけ処理したい場合は、片方の引数を省略できます。

  • 成功時のみ処理したい場合:
    javascript
    promise.then(onFulfilled); // onRejected を省略

    この場合、Promiseが失敗しても何も起きません(エラーは捕捉されません)。

  • 失敗時のみ処理したい場合:
    javascript
    promise.then(null, onRejected); // onFulfilled を null または undefined にする

    成功しても何も起きません。失敗した場合のみonRejectedが実行されます。失敗時の処理だけを行いたい場合は、後述する.catch()メソッドを使うのが一般的で推奨されます。

.then()が返す値: 新しいPromiseを返すことの重要性

.then()メソッドの非常に重要な特性は、常に新しいPromiseオブジェクトを返すという点です。

“`javascript
const promise1 = new Promise((resolve) => resolve(1));
const promise2 = promise1.then((value) => {
console.log(“promise1の成功:”, value); // 1
return value + 1; // 値を返す
});

const promise3 = promise2.then((value) => {
console.log(“promise2の成功:”, value); // 2
// 何も返さない(暗黙的に undefined を返す)
});

const promise4 = promise3.then((value) => {
console.log(“promise3の成功:”, value); // undefined
// 新しいPromiseを返す
return new Promise((resolve) => {
setTimeout(() => resolve(“新しいPromiseの結果”), 500);
});
});

const promise5 = promise4.then((value) => {
console.log(“promise4の成功:”, value); // “新しいPromiseの結果”
});

// 実行結果:
// promise1の成功: 1
// promise2の成功: 2
// promise3の成功: undefined
// (0.5秒後に)
// promise4の成功: 新しいPromiseの結果
“`

この特性こそが、Promiseチェーンを構築し、複数の非同期処理を順序立てて実行可能にする基盤となっています。.then()が返す新しいPromiseは、その.then()のコールバック関数の実行結果に基づいてSettledされます。

具体的には、.then(onFulfilled, onRejected)が返す新しいPromiseは、以下のルールでSettledされます。

  1. onFulfilled または onRejected コールバックが同期的に値を返した場合:

    • その値が新しいPromiseの成功時の値となります。返された値がそれ自身Promiseオブジェクトでない限り、新しいPromiseはFulfilled状態になります。
    • 例: return value + 1; のようにプリミティブな値やオブジェクトを返した場合。
  2. onFulfilled または onRejected コールバックが新しいPromiseを返した場合:

    • .then()が返すPromiseは、コールバックが返したその新しいPromiseの状態に追随します。つまり、返されたPromiseがFulfilledになれば、.then()が返すPromiseも同じ値でFulfilledになります。返されたPromiseがRejectedになれば、.then()が返すPromiseも同じ理由でRejectedになります。
    • 例: return anotherPromise; のようにPromiseを返した場合。これは、ある非同期処理の完了を待って、別の非同期処理を開始し、その結果を次の.then()に渡したい場合に非常に役立ちます。
  3. onFulfilled または onRejected コールバックが何も返さなかった場合 (暗黙的に undefined を返す):

    • .then()が返す新しいPromiseは、undefinedを値としてFulfilled状態になります。
  4. onFulfilled または onRejected コールバック内でエラーがスローされた場合 (throw):

    • .then()が返す新しいPromiseは、スローされたエラーを理由としてRejected状態になります。これは、同期的なエラーや、非同期処理中に発生した捕捉されなかったエラーをPromiseチェーンで伝播させるメカニズムです。

この「.then()が新しいPromiseを返し、そのPromiseの状態はコールバックの戻り値によって決まる」という挙動は、Promiseチェーンの理解において最も重要です。

Promiseチェーン:複数の非同期処理を連携させる

.then()メソッドが新しいPromiseを返すという特性を利用して、複数の非同期処理を連続して実行することができます。これが「Promiseチェーン」です。Promiseチェーンを使うことで、Callback Hellのような深いネストを避け、処理の流れを上から下へ、より直線的に記述できます。

基本的なPromiseチェーンは以下のようになります。

javascript
asyncOperation1() // Promiseを返す関数と仮定
.then(result1 => {
console.log("最初の操作が成功:", result1);
// result1 を使って次の非同期操作を開始し、その Promise を返す
return asyncOperation2(result1);
})
.then(result2 => {
console.log("2番目の操作が成功:", result2);
// result2 を使ってさらに次の非同期操作を開始し、その Promise を返す
return asyncOperation3(result2);
})
.then(result3 => {
console.log("3番目の操作が成功:", result3);
// 全ての操作が成功
console.log("全ての操作が完了しました!最終結果:", result3);
})
.catch(error => {
// チェーンのどこかで発生したエラーをここでまとめて捕捉
console.error("エラーが発生しました:", error);
});

この構造を見ると、Callback Hellと比較して、コードが横に広がらず、処理の流れが分かりやすいことがわかります。各.then()ブロックは、前のステップのPromiseが成功した場合にのみ実行されます。そして、各.then()ブロックの戻り値が、次の.then()ブロックに渡されるPromiseの結果となります。

Promiseチェーンにおける戻り値の挙動の確認

Promiseチェーンの各ステップで、onFulfilledコールバックが何を返すかによって、次の.then()に何が渡されるかが変わります。

  1. 値を返す場合 (Promise以外):
    javascript
    Promise.resolve(1)
    .then(value1 => {
    console.log("ステップ1:", value1); // 1
    return value1 + 1; // 値を返す
    })
    .then(value2 => {
    console.log("ステップ2:", value2); // 2 (前の then から返された値)
    return value2 * 2;
    })
    .then(value3 => {
    console.log("ステップ3:", value3); // 4
    });

    前の.then()のコールバックが返した値(この例ではvalue1 + 1の結果である2)が、次の.then()のコールバックの引数(value2)として渡されます。

  2. 新しいPromiseを返す場合:
    javascript
    Promise.resolve(1)
    .then(value1 => {
    console.log("ステップ1:", value1); // 1
    // 新しいPromiseを返す(例えば非同期操作)
    return new Promise(resolve => {
    setTimeout(() => resolve(value1 + 10), 500);
    });
    })
    .then(value2 => {
    console.log("ステップ2:", value2); // 11 (新しい Promise の解決値)
    // また別の新しいPromiseを返す
    return new Promise((resolve, reject) => {
    setTimeout(() => reject("ステップ2で失敗"), 300);
    });
    })
    .then(value3 => {
    // 前の Promise が reject されたので、このステップは実行されない
    console.log("ステップ3:", value3);
    })
    .catch(error => {
    // ステップ2で返された Promise の reject がここで捕捉される
    console.error("チェーンでエラー:", error); // "ステップ2で失敗"
    });

    ステップ1の.then()は新しいPromiseを返しています。次のステップ2の.then()は、その返されたPromiseがSettled状態になるまで待機します。返されたPromiseがFulfilled (resolve(value1 + 10)) されたため、その値 (11) がステップ2の.then()の引数 (value2) として渡されます。
    ステップ2の.then()は別の新しいPromiseを返していますが、こちらはRejected (reject("ステップ2で失敗")) されました。そのため、それに続くステップ3の.then()onFulfilledコールバックは実行されず、代わりに.catch()ブロックが実行されてエラーが捕捉されています。

  3. 何も返さない場合 (暗黙的に undefined を返す):
    javascript
    Promise.resolve("開始")
    .then(value1 => {
    console.log("ステップ1:", value1); // "開始"
    // return しない
    })
    .then(value2 => {
    console.log("ステップ2:", value2); // undefined
    });

    ステップ1の.then()が何も返さない(あるいは明示的にreturn undefined;とする)場合、次に続くステップ2の.then()にはundefinedが渡されます。

  4. エラーをスローする場合:
    javascript
    Promise.resolve("開始")
    .then(value1 => {
    console.log("ステップ1:", value1); // "開始"
    throw new Error("ステップ1で同期的なエラー"); // エラーをスロー
    })
    .then(value2 => {
    // 前のステップでエラーがスローされたため、このステップは実行されない
    console.log("ステップ2:", value2);
    })
    .catch(error => {
    // スローされたエラーがここで捕捉される
    console.error("チェーンでエラー:", error.message); // "ステップ1で同期的なエラー"
    });

    ステップ1の.then()のコールバック内でエラーがスローされた場合、そのエラーは次の.then()onFulfilledコールバックをスキップし、チェーンを下っていきます。そして、最初のonRejectedコールバック(または.catch())に捕捉されるまで伝播します。

これらの挙動を理解することが、複雑なPromiseチェーンを正確に記述し、意図通りに非同期処理を制御するために非常に重要です。

Promiseチェーンは、非同期処理の依存関係を非常に分かりやすく表現できます。「この処理が終わったら、その結果を使って次の処理をする」という流れが、コードの見た目と実行順序で一致するため、Callback Hellのような追跡の困難さが解消されます。

エラーハンドリング:非同期処理の失敗を適切に処理する

非同期処理では、ネットワークエラー、タイムアウト、サーバーからのエラーレスポンスなど、様々な理由で処理が失敗する可能性があります。Promiseチェーンを安全に利用するためには、これらの失敗を適切に捕捉し、処理するメカニズムが不可欠です。

Promiseにおいて失敗を扱う方法は主に2つあります。

  1. .then()の第2引数(onRejectedコールバック)を使う
  2. .catch()メソッドを使う

1. .then()の第2引数によるエラーハンドリング

先ほども触れたように、.then()メソッドは成功時のコールバック (onFulfilled) と失敗時のコールバック (onRejected) の両方を受け取ることができます。

javascript
somePromise.then(
result => {
console.log("成功:", result);
},
error => {
console.error("失敗:", error); // ここでエラーを捕捉
}
);

この方法でエラーを処理する場合、その.then()メソッドよりも前のチェーンで発生したエラーを捕捉できます。しかし、その.then()自体のonFulfilledコールバック内で発生したエラーや、その.then()が返す新しいPromiseがRejectedになった場合は、このonRejectedコールバックでは捕捉できません。そのエラーはさらにチェーンを下って、次の.then()onRejectedコールバックや.catch()に伝播します。

javascript
Promise.reject("最初の失敗") // 失敗から始まるPromise
.then(
result1 => console.log("ステップ1 成功:", result1),
error1 => {
console.error("ステップ1 失敗:", error1); // "最初の失敗" を捕捉
return "エラーから復帰"; // 値を返すと、次に続く then は成功とみなされる
}
)
.then(
result2 => {
console.log("ステップ2 成功:", result2); // "エラーから復帰"
throw new Error("ステップ2で同期的なエラー"); // ここでエラーをスロー
},
error2 => console.error("ステップ2 失敗:", error2) // ステップ1の onRejected が値を返したので、これは実行されない
)
.then(
result3 => console.log("ステップ3 成功:", result3),
error3 => console.error("ステップ3 失敗:", error3.message) // "ステップ2で同期的なエラー" を捕捉
);

この例のように、.then()の第2引数でエラーを処理すると、その後のチェーンが再開される可能性があります(onRejectedコールバックが値を返した場合)。しかし、これは少し扱いにくい場合があり、特にチェーン全体を通してエラーをまとめて処理したい場合には不向きです。

2. .catch() メソッドを使う

.catch()メソッドは、実際には .then(null, onRejected) の糖衣構文(シンタックスシュガー)です。つまり、成功時のコールバックを持たず、失敗時のコールバックのみを指定するための便利な方法です。

javascript
promise.catch(onRejected); // 失敗時のコールバックのみを指定

.catch()メソッドの最大の利点は、チェーンのどこかで発生したエラーを、まとめて捕捉できるという点です。通常、Promiseチェーンの最後に.catch()を配置することで、チェーンのどのステップでエラーが発生しても、そのエラーが.catch()に伝播し、一元的に処理できるようになります。

javascript
asyncOperation1()
.then(result1 => {
console.log("ステップ1 成功:", result1);
return asyncOperation2(result1); // asyncOperation2 が失敗する可能性
})
.then(result2 => {
console.log("ステップ2 成功:", result2);
// ここで同期的なエラーが発生する可能性
if (result2.someProperty === undefined) {
throw new Error("不正なデータ形式");
}
return asyncOperation3(result2); // asyncOperation3 が失敗する可能性
})
.then(result3 => {
console.log("ステップ3 成功:", result3);
})
.catch(error => {
// ステップ1, 2, 3 のいずれかで発生したエラーがここに集まる
console.error("チェーン全体でエラーが発生しました:", error);
// エラーに応じた処理(ユーザーへの通知、ログ記録など)
});

このパターンは、Promiseチェーンにおけるエラーハンドリングの最も一般的で推奨される方法です。これにより、成功時の処理の流れとエラー処理のロジックを分離でき、コードの可読性と保守性が向上します。

.finally() メソッド

Promiseには、.then().catch()の他に、.finally()メソッドもあります。.finally()に登録されたコールバック関数は、PromiseがFulfilledまたはRejectedのどちらの状態になっても、最終的に実行されます。

javascript
somePromise
.then(result => {
console.log("成功:", result);
})
.catch(error => {
console.error("失敗:", error);
})
.finally(() => {
console.log("Promiseの処理が完了しました (成功でも失敗でも)");
// ここでリソースの解放などの後処理を行う(ローディング表示を消すなど)
});

.finally()のコールバックは引数を受け取りません(Promiseの結果の値や理由にはアクセスできません)。また、.finally()は元のPromiseの値や理由を透過させます。つまり、.finally()は常に新しいPromiseを返しますが、その新しいPromiseは、.finally()が呼び出された元のPromiseと同じ値でFulfilledになるか、または同じ理由でRejectedになります(ただし、.finally()のコールバック内でエラーが発生した場合やPromiseが返された場合はその限りではありません)。

.finally()は、例えば非同期処理の開始時に表示したローディングスピナーを、成功または失敗のどちらの場合でも非表示にしたい、といったクリーンアップ処理に非常に役立ちます。

どこでエラーを捕捉するか?

エラーハンドリングの戦略は、アプリケーションの要件によって異なります。

  • チェーンの最後で一括捕捉 (.catch()): 最も一般的で、シンプルにエラーをログに記録したり、ユーザーに一般的なエラーメッセージを表示したりする場合に適しています。ほとんどのエラーハンドリングはこのパターンで十分です。
  • チェーンの途中で捕捉 (.then(null, onRejected) または .catch()): 特定の非同期処理の失敗から回復し、代替処理を実行したい場合に使います。例えば、「API呼び出しが失敗したら、キャッシュデータを使う」といったロジックを実装する場合です。途中でエラーを捕捉して値を返すと、それに続く.then()チェーンは成功として実行されます。

javascript
fetch('/api/data')
.catch(error => {
console.warn("API呼び出し失敗、キャッシュを使用します:", error);
// エラーから復帰し、代替データを返すPromiseを返す
return getCacheData();
})
.then(data => {
console.log("使用するデータ:", data); // APIデータまたはキャッシュデータ
updateUI(data);
})
.catch(finalError => {
// キャッシュデータの取得も失敗した場合など、最終的なエラーを捕捉
console.error("データ取得に致命的なエラー:", finalError);
showErrorPage();
});

この例では、最初のfetchが失敗した場合に.catch()でエラーを捕捉し、代替の非同期処理(getCacheData())を実行しています。getCacheData()がPromiseを返す場合、そのPromiseの解決値が次の.then(data => ...)に渡され、処理が継続します。

捕捉されなかったエラー (Unhandled Rejection)

PromiseがRejected状態になったにも関わらず、そのPromiseチェーン全体を通してどこにもonRejectedコールバックや.catch()メソッドでエラーが捕捉されなかった場合、そのエラーは「Unhandled Rejection(ハンドルされなかった拒否)」として扱われます。

これは、多くのJavaScript実行環境(ブラウザ、Node.js)で警告やエラーとして報告されます。ブラウザではunhandledrejectionイベント、Node.jsではunhandledRejectionイベントが発火し、プログラムがクラッシュする原因となることもあります。

プロダクション環境では、Unhandled Rejectionが発生しないように、常にPromiseチェーンの最後に.catch()ブロックを追加することが推奨されます。これにより、予期しないエラーが発生しても、少なくともそのエラーをログに記録するなどして、問題の発見に役立てることができます。

javascript
// 例:Unhandled Rejectionが発生する場合
const promise = Promise.reject(new Error("捕捉されないエラー"));
// ここに .catch() がないと Unhandled Rejection になる

javascript
// 例:Unhandled Rejectionを防ぐ場合
const promise = Promise.reject(new Error("捕捉されないエラー"));
promise.catch(error => {
console.error("捕捉されたエラー:", error); // エラーが捕捉される
});

Promiseのエラーハンドリングは、非同期処理を安定して動作させるために非常に重要です。.catch()を効果的に利用して、予期せぬ事態にも対応できる堅牢なコードを書きましょう。

.then()の応用と関連メソッド

Promiseは.then()メソッドだけでなく、非同期処理をより柔軟かつ強力に扱うための様々な静的メソッドも提供しています。これらのメソッドと.then()を組み合わせて使うことで、様々な非同期処理パターンを実現できます。

Promise.resolve() / Promise.reject() と .then()

Promise.resolve(value)は、渡されたvalueで即座にFulfilled状態となるPromiseを返します。
Promise.reject(reason)は、渡されたreasonで即座にRejected状態となるPromiseを返します。

これらは、例えば非同期処理を行う関数が、同期的に結果が分かっている場合にPromiseを返す必要があるが、新しいPromiseを作成するまでもない、といった場面で役立ちます。

``javascript
function getUserData(userId) {
if (userId < 0) {
// エラーの場合は即座にRejectedなPromiseを返す
return Promise.reject(new Error("無効なユーザーID"));
}
// 正常な場合は、非同期でデータを取得するPromiseを返す
return fetch(
/api/users/${userId}`).then(response => response.json());
}

getUserData(-1)
.then(data => console.log(“ユーザーデータ:”, data)) // 実行されない
.catch(error => console.error(“エラー:”, error.message)); // “無効なユーザーID” が表示される

getUserData(123)
.then(data => console.log(“ユーザーデータ:”, data)) // fetchが成功した場合に実行される
.catch(error => console.error(“エラー:”, error));
“`

また、Promise.resolve()はPromiseLikeなオブジェクト(.thenメソッドを持つオブジェクト)を受け取ると、そのPromiseLikeオブジェクトに追随する新しいPromiseを返すという特性があります。これは、Promiseとそうでないもの、あるいは異なるPromise実装間での相互運用性を高めるのに役立ちます。

“`javascript
// PromiseLikeなオブジェクトの例
const promiseLike = {
then: function(resolve, reject) {
setTimeout(() => resolve(“PromiseLikeからの値”), 100);
}
};

Promise.resolve(promiseLike)
.then(value => {
console.log(“Promise.resolveでラップされたPromiseLikeからの値:”, value); // “PromiseLikeからの値”
});

// これは以下のコードとほぼ同じ挙動
new Promise(resolve => {
setTimeout(() => resolve(“PromiseLikeからの値”), 100);
})
.then(value => {
console.log(“手動でPromiseを作成した場合からの値:”, value);
});
“`

Promise.all() と .then()

Promise.all(iterable)は、複数のPromiseオブジェクトを含むイテラブル(配列など)を受け取り、新しいPromiseを1つ返します。

  • 返されるPromiseは、イテラブル内の全てのPromiseがFulfilledになった場合にFulfilledになります。その解決値は、元のPromiseが解決された順序と同じ順序で、それぞれの解決値を要素とする配列になります。
  • イテラブル内のどれか1つでもPromiseがRejectedになった場合、返されるPromiseは即座にRejectedになります。その理由は、最初にRejectedになったPromiseの理由と同じになります。

これは、複数の非同期処理を並行して実行し、それら全てが完了するのを待ってから次の処理に進みたい場合に非常に便利です。

“`javascript
const promiseA = new Promise(resolve => setTimeout(() => resolve(“A”), 1000));
const promiseB = new Promise(resolve => setTimeout(() => resolve(“B”), 500));
const promiseC = new Promise(resolve => setTimeout(() => resolve(“C”), 1500));

Promise.all([promiseA, promiseB, promiseC])
.then(results => {
// 全て成功した場合に実行
console.log(“Promise.all – 全て成功:”, results); // [“A”, “B”, “C”]
// results[0] は promiseA の結果, results[1] は promiseB の結果 …
})
.catch(error => {
// どれか一つでも失敗した場合に実行
console.error(“Promise.all – いずれか失敗:”, error);
});

// 別の例(失敗を含む)
const promiseD = Promise.resolve(“D”);
const promiseE = Promise.reject(“Eは失敗”); // 即座に失敗
const promiseF = Promise.resolve(“F”);

Promise.all([promiseD, promiseE, promiseF])
.then(results => {
console.log(“Promise.all – 全て成功:”, results); // 実行されない
})
.catch(error => {
console.error(“Promise.all – いずれか失敗:”, error); // “Eは失敗” が表示される
});
“`

Promise.all()の結果を.then()で受けることで、並行実行された非同期処理の完了後に続く処理を記述できます。

Promise.race() と .then()

Promise.race(iterable)も、複数のPromiseオブジェクトを含むイテラブルを受け取り、新しいPromiseを1つ返します。

  • 返されるPromiseは、イテラブル内のPromiseのうち、最初にSettled状態(FulfilledまたはRejected)になったものと同じ状態になります。解決値または理由は、その最初にSettledになったPromiseのものと同じになります。

これは、複数の非同期処理を並行して実行し、最も早く完了したものだけを使いたい場合や、タイムアウト処理を実装する場合などに便利です。

“`javascript
const promiseG = new Promise(resolve => setTimeout(() => resolve(“G (遅い)”), 2000));
const promiseH = new Promise(resolve => setTimeout(() => resolve(“H (速い)”), 500));

Promise.race([promiseG, promiseH])
.then(result => {
console.log(“Promise.race – 最初に成功:”, result); // “H (速い)”
})
.catch(error => {
console.error(“Promise.race – 最初に失敗:”, error); // 最初に失敗したPromiseがあればこちらが実行
});

// タイムアウト処理の例
const timeoutPromise = (ms) => new Promise((_, reject) => setTimeout(() => reject(new Error(タイムアウト (${ms}ms))), ms));

Promise.race([
fetch(‘/api/slow-endpoint’).then(response => response.json()),
timeoutPromise(3000) // 3秒でタイムアウト
])
.then(data => {
console.log(“データ取得成功:”, data);
})
.catch(error => {
console.error(“データ取得失敗:”, error); // タイムアウトまたはfetchエラー
});
“`

Promise.race()の結果を.then()で受けることで、最初に完了したPromiseの結果に基づいて後続の処理を分岐させることができます。

Promise.any() と .then()

Promise.any(iterable) (ECMAScript 2021 で導入) も複数のPromiseを受け取り、新しいPromiseを1つ返します。

  • 返されるPromiseは、イテラブル内のPromiseのうち、最初にFulfilledになった場合にFulfilledになります。その解決値は、その最初にFulfilledになったPromiseの値と同じになります。
  • イテラブル内の全てのPromiseがRejectedになった場合、返されるPromiseはRejectedになります。この場合、AggregateErrorという特別なエラーオブジェクトが理由となります。

これは、複数のリソースの中から最初に利用可能になったものを使いたい場合などに便利です。

“`javascript
const promiseI = new Promise((, reject) => setTimeout(() => reject(“Iは遅れて失敗”), 1000));
const promiseJ = new Promise(resolve => setTimeout(() => resolve(“Jは最初に成功”), 500));
const promiseK = new Promise((
, reject) => setTimeout(() => reject(“Kはさらに遅れて失敗”), 1500));

Promise.any([promiseI, promiseJ, promiseK])
.then(result => {
console.log(“Promise.any – 最初に成功:”, result); // “Jは最初に成功”
})
.catch(error => {
// 全て失敗した場合に実行される (error は AggregateError)
console.error(“Promise.any – 全て失敗:”, error.errors); // エラーの配列
});
“`

Promise.any()の結果を.then()で受けることで、複数の候補のうち最初に成功したPromiseの結果に基づいて処理を進めることができます。

Promise.allSettled() と .then()

Promise.allSettled(iterable) (ECMAScript 2020 で導入) も複数のPromiseを受け取り、新しいPromiseを1つ返します。

  • 返されるPromiseは、イテラブル内の全てのPromiseがSettled状態(FulfilledまたはRejected)になった場合にFulfilledになります。
  • 返されるPromiseの解決値は、元のPromiseがSettledされた順序と同じ順序で、それぞれの結果を表すオブジェクトの配列になります。各結果オブジェクトは以下の形式をとります。
    • PromiseがFulfilledの場合: { status: 'fulfilled', value: 解決値 }
    • PromiseがRejectedの場合: { status: 'rejected', reason: 拒否理由 }

これは、複数の非同期処理を並行して実行し、その結果が成功か失敗かに関わらず、全ての結果を取得して処理を進めたい場合に便利です。

“`javascript
const promiseL = new Promise(resolve => setTimeout(() => resolve(“Lは成功”), 800));
const promiseM = new Promise((_, reject) => setTimeout(() => reject(“Mは失敗”), 400));
const promiseN = new Promise(resolve => setTimeout(() => resolve(“Nは成功”), 1200));

Promise.allSettled([promiseL, promiseM, promiseN])
.then(results => {
// 全てのPromiseが Settled された後に実行される
console.log(“Promise.allSettled – 全て完了:”, results);
// results の内容は以下のようになる (順序は入力と同じ)
// [
// { status: ‘fulfilled’, value: ‘Lは成功’ },
// { status: ‘rejected’, reason: ‘Mは失敗’ },
// { status: ‘fulfilled’, value: ‘Nは成功’ }
// ]

// 結果を個別に処理
results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log(`成功: ${result.value}`);
  } else {
    console.error(`失敗: ${result.reason}`);
  }
});

});
“`

Promise.allSettled()の結果を.then()で受けることで、個々のPromiseの成功/失敗状態と結果をまとめて取得し、後続の処理で一つずつ確認・処理できます。

これらの関連メソッドと.then()を組み合わせることで、単一の非同期処理だけでなく、複数の非同期処理を協調させて実行し、その結果を適切に処理する強力なパターンを構築できます。

実践的なPromiseと.then()の活用例

ここからは、実際の開発でよく遭遇するシナリオで、Promiseと.then()がどのように活用されるかを見ていきましょう。

1. Fetch APIを使ったAPI呼び出しと.then()チェーン

ブラウザ環境でのHTTP通信の標準であるFetch APIは、Promiseベースです。Fetch APIは.then()チェーンと非常に相性が良く、非同期のネットワークリクエストとレスポンス処理を簡潔に記述できます。

javascript
// ユーザー一覧を取得し、最初のユーザーの投稿を取得する例
fetch('https://jsonplaceholder.typicode.com/users') // ユーザー一覧を取得
.then(response => {
// レスポンスのステータスコードを確認
if (!response.ok) {
// エラーレスポンスの場合はエラーをスローし、PromiseをRejectedにする
throw new Error(`HTTP error! status: ${response.status}`);
}
// レスポンスボディをJSONとしてパースし、そのPromiseを返す
return response.json();
})
.then(users => {
console.log("ユーザー一覧取得成功:", users);
if (users.length === 0) {
throw new Error("ユーザーが見つかりません");
}
const firstUser = users[0];
console.log("最初のユーザー:", firstUser);
// 最初のユーザーの投稿を取得する別の非同期処理を開始し、そのPromiseを返す
return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${firstUser.id}`);
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(posts => {
console.log("最初のユーザーの投稿取得成功:", posts);
// 全ての処理が成功
console.log("ユーザーと投稿の取得完了");
})
.catch(error => {
// チェーンのどこかで発生したエラーを捕捉
console.error("処理中にエラーが発生しました:", error);
// エラーメッセージをユーザーに表示するなど
alert(`データの取得中にエラーが発生しました: ${error.message}`);
})
.finally(() => {
console.log("API呼び出し処理が終了しました。");
// ローディング表示を非表示にするなどの後処理
});

この例では、.then()チェーンを使って以下の処理を順序通り実行しています。
1. ユーザー一覧を取得 (fetch)
2. レスポンスをチェックし、JSONとしてパース (.then(response => response.json()))
3. パースしたユーザーデータを使って、最初のユーザーIDを取得
4. 最初のユーザーの投稿一覧を取得 (fetch)
5. レスポンスをチェックし、JSONとしてパース (.then(response => response.json()))
6. パースした投稿データを処理

途中でエラーが発生した場合(ネットワークエラー、HTTPステータスコードがokでない、ユーザーが見つからないなど)、エラーは.catch()ブロックに伝播し、一元的に処理されます。また、成功・失敗に関わらず最後に.finally()が実行されます。

Callback Hellでこれを書こうとすると、fetchのネスト、response.json()のネスト、それぞれのエラーチェックなどが深く入り組み、非常に読みにくいコードになるでしょう。Promiseチェーンは、このような依存関係のある非同期処理をエレガントに解決します。

2. ファイル読み込み(Node.js fsモジュール)と.then()

Node.js環境でも、多くの非同期APIはPromiseをサポートしています(または、コールバックベースのAPIをPromise化するユーティリティが提供されています)。fs/promisesモジュールを使うと、ファイル操作をPromiseベースで行えます。

“`javascript
const fs = require(‘fs/promises’);

async function processFile(filePath) {
let fileContent;
let parsedData;

try {
// ファイルを読み込む (Promiseを返す)
fileContent = await fs.readFile(filePath, ‘utf8’);
console.log(“ファイル読み込み成功”);

// 読み込んだ内容をJSONとしてパース
parsedData = JSON.parse(fileContent);
console.log("JSONパース成功");

// パースしたデータを使って何か別の非同期処理
// 例えば、別のファイルに書き出す
await fs.writeFile('output.txt', `処理結果: ${JSON.stringify(parsedData)}`);
console.log("処理結果書き出し成功");

return parsedData; // 最終的な結果を返す

} catch (error) {
// 読み込み、パース、書き出しのどこかでエラーが発生した場合
console.error(“ファイル処理中にエラー:”, error);
throw error; // エラーを呼び出し元に再スロー
} finally {
console.log(“ファイル処理が終了しました。”);
}
}

// async/awaitを使わない .then() チェーンの例
function processFileWithThen(filePath) {
return fs.readFile(filePath, ‘utf8’) // Promiseを返す
.then(fileContent => {
console.log(“ファイル読み込み成功”);
// 同期処理だが、Promiseチェーンの一部として実行
const parsedData = JSON.parse(fileContent);
console.log(“JSONパース成功”);
// 次のステップにパース結果を渡す
return parsedData;
})
.then(parsedData => {
// 前のステップからパース結果を受け取る
console.log(“パース済みデータを使って書き出し…”);
// 新しい非同期処理(書き出し)を開始し、そのPromiseを返す
return fs.writeFile(‘output_then.txt’, 処理結果 (then): ${JSON.stringify(parsedData)});
})
.then(() => {
console.log(“処理結果書き出し成功 (then)”);
// 最終的な結果(ここでは特に返さないが、前のステップの結果を渡すことも可能)
})
.catch(error => {
// チェーンのどこかでエラーが発生した場合
console.error(“ファイル処理中にエラー (then):”, error);
throw error; // エラーを再スロー
})
.finally(() => {
console.log(“ファイル処理が終了しました (then)。”);
});
}

// 実行例
processFileWithThen(‘input.json’) // input.json は存在する JSON ファイルとする
.then(() => console.log(“全てのファイル処理完了”))
.catch(() => console.error(“ファイル処理失敗”));
“`

この例のように、Promiseを返す非同期関数を.then()で繋げることで、ファイル操作のような一連の非同期処理ステップを順番に実行し、結果を受け渡すことができます。JSONパースのような同期処理も、.then()のコールバック内で行い、その結果を返すことでチェーンに組み込めます。

これらの実践例からもわかるように、Promiseと.then()は、現代のJavaScriptにおける非同期処理を記述するためのデファクトスタンダードと言えます。Callback Hellを回避し、コードを読みやすく、管理しやすくする上で非常に強力なツールです。

async/await と Promise/.then()

JavaScriptにES2017で導入されたasync/await構文は、Promiseの上に構築されたシンタックスシュガーです。async/awaitを使うと、Promiseベースの非同期処理を、まるで同期処理であるかのように見かけ上、直線的に記述できます。これは、Promiseチェーンをさらに読みやすくするための強力なツールです。

しかし、async/awaitはPromiseと.then()を置き換えるものではありません。むしろ、async/awaitは内部的にPromiseを利用しており、.then().catch()で実現していた非同期処理の待機と結果の受け渡しを、より直感的な構文で表現することを可能にします。

async/awaitの基本

  • asyncキーワードは、関数を非同期関数として定義します。非同期関数は必ずPromiseを返します。関数内でエラーがスローされた場合は、RejectedなPromiseを返します。
  • awaitキーワードは、async関数の中でしか使用できません。await式の右辺には通常Promiseを指定します。awaitはPromiseがSettled状態になるまで非同期的に待機し、PromiseがFulfilledになった場合はその解決値を返します。Rejectedになった場合はエラーをスローします。

async/awaitを使ったコードとPromiseチェーンを使ったコードの比較

先ほどのAPI呼び出しの例をasync/awaitで書き直してみましょう。

“`javascript
// Promiseチェーンの例(再掲)
function fetchUserPostsThen(userId) {
let userData; // ユーザーデータを保持するための変数

return fetch(https://jsonplaceholder.typicode.com/users/${userId})
.then(response => {
if (!response.ok) throw new Error(HTTP error! status: ${response.status});
return response.json();
})
.then(user => {
userData = user; // ユーザーデータを一時的に保持
console.log(“ユーザー取得成功:”, user);
return fetch(https://jsonplaceholder.typicode.com/posts?userId=${user.id});
})
.then(response => {
if (!response.ok) throw new Error(HTTP error! status: ${response.status});
return response.json();
})
.then(posts => {
console.log(“投稿取得成功:”, posts);
// ユーザーデータと投稿データを組み合わせて返すなど
return { user: userData, posts: posts };
});
}

// async/awaitを使った例
async function fetchUserPostsAsync(userId) {
try {
// ユーザーを取得
const userResponse = await fetch(https://jsonplaceholder.typicode.com/users/${userId});
if (!userResponse.ok) {
throw new Error(HTTP error! status: ${userResponse.status});
}
const user = await userResponse.json();
console.log(“ユーザー取得成功:”, user);

// 投稿を取得
const postsResponse = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`);
if (!postsResponse.ok) {
  throw new Error(`HTTP error! status: ${postsResponse.status}`);
}
const posts = await postsResponse.json();
console.log("投稿取得成功:", posts);

// 結果を返す
return { user, posts };

} catch (error) {
// チェーンのどこかで発生したエラーを捕捉
console.error(“処理中にエラーが発生しました:”, error);
// エラーを再スローして、async関数のPromiseをRejectedにする
throw error;
} finally {
console.log(“async関数による処理が終了しました。”);
}
}

// async関数の呼び出しと .then() / .catch() で結果を扱う
fetchUserPostsAsync(1)
.then(data => {
console.log(“fetchUserPostsAsync 成功:”, data);
})
.catch(error => {
console.error(“fetchUserPostsAsync 失敗:”, error);
});
“`

async/awaitを使ったコードは、Promiseチェーンのコードと比較して、処理の流れがより直線的で、同期処理に近い見た目になります。変数(例: user, posts)を使って非同期処理の結果を受け渡しできるため、.then()の引数やクロージャを使う必要が減り、可読性が向上する傾向があります。エラーハンドリングも、try...catchブロックという馴染み深い構文で行えるのが利点です。

どちらを使うべきか?

async/awaitはPromiseチェーンの優れた代替手段であり、多くの場面でコードを簡潔に、読みやすくします。特に、依存関係のある一連の非同期処理を順番に実行したい場合には、async/awaitが非常に有効です。

しかし、Promiseチェーンや.then()が不要になるわけではありません。

  • 並列処理: 複数の非同期処理を並行して実行し、全ての結果を待つ場合は、Promise.all()を使うのが適切です。async/awaitでこれを書こうとすると、各awaitが前の処理の完了を待ってしまうため、意図せず直列処理になってしまう可能性があります。
    javascript
    // async/awaitで並列化する場合の一般的なパターン
    async function fetchMultipleData() {
    const [users, posts] = await Promise.all([
    fetch('/api/users').then(res => res.json()),
    fetch('/api/posts').then(res => res.json())
    ]);
    console.log("両方のデータ取得完了:", { users, posts });
    }
    fetchMultipleData();

    このように、async/awaitの中でもPromise.all()や他のPromise静的メソッドは組み合わせて使用されます。
  • コールバック関数との連携: イベントリスナーなどでPromiseを返す非同期処理を実行し、その完了を待たずにすぐに戻るような場合、.then().catch()を使う方が自然な場合があります。
  • Promiseの基礎理解: async/awaitはあくまでPromiseのシンタックスシュガーです。async/awaitを効果的に使うためには、Promiseの基本的な概念、状態、そして.then()が新しいPromiseを返すという仕組みを理解しておくことが不可欠です。async/awaitを使ったコードで予期しない挙動に遭遇した場合、その原因はPromiseの基本的な動作に起因することが多いためです。

結論として、現代のJavaScript開発では、async/awaitを非同期処理の記述の第一選択肢としつつも、Promiseチェーンや.then().catch()、そしてPromise静的メソッドも、適切に使い分けることが重要です。async/awaitはPromiseをより使いやすくするツールであり、Promiseの理解がその基盤となります。特に、この記事で詳しく解説した.then()が新しいPromiseを返し、その状態がコールバックの戻り値に依存するという仕組みは、async/awaitのawaitがどのように機能するかの理解にも繋がります。

よくある落とし穴とデバッグ

Promiseと.then()を使う上で、開発者が陥りがちな落とし穴がいくつかあります。それらを理解し、回避することで、より堅牢でデバッグしやすい非同期コードを書くことができます。

  1. .then()のコールバック内で値を返し忘れる、または間違った値を返す:
    Promiseチェーンにおいて、前の.then()のコールバックが返す値が、次に続く.then()onFulfilledコールバックに引数として渡されます。ここで値を返し忘れる(または明示的にreturn undefined;とする)と、次の.then()にはundefinedが渡されてしまい、意図しない挙動につながることがあります。特に、非同期処理(新しいPromise)を返すつもりが、同期的な値を返してしまったり、その逆だったりすることもあります。
    対策:.then()のコールバックで、次に渡したい値を明示的にreturnすることを意識しましょう。特に、非同期操作の結果を次に渡したい場合は、その非同期操作が返すPromiseをreturnする必要があります。async/awaitを使う場合も、awaitの戻り値を次の処理に使うために変数に代入したり、関数の戻り値としてreturnしたりすることを忘れないようにしましょう。

  2. Promiseチェーンの途中でエラーを適切に捕捉しない:
    PromiseがRejectedになったにも関わらず、エラーが.then()の第2引数や.catch()で捕捉されないままチェーンの最後まで到達すると、Unhandled Rejectionが発生する可能性があります。
    対策: Promiseチェーンの最後には必ず.catch()ブロックを追加しましょう。これにより、チェーンのどの段階でエラーが発生しても、一元的に捕捉してログに記録するなどの対応ができます。特定のステップのエラーから復帰したい場合は、そのステップの直後に.catch()または.then(null, onRejected)を配置し、onRejectedコールバック内でエラーを処理し、次に続くPromiseチェーンを継続するための値を返すようにします。

  3. Promiseチェーン内で同期処理と非同期処理を混同する:
    .then()のコールバック内では、同期処理も非同期処理(新しいPromiseを返す)も実行できますが、その結果が次にどう渡されるかのルールを正しく理解していないと混乱します。同期的に値を返すと、次に続く.then()はすぐに実行されますが、Promiseを返すと、そのPromiseがSettledされるまで次の.then()は待機します。
    対策:.then()のコールバックの戻り値が何であるか(値かPromiseか)を明確に意識しましょう。特に、あるステップで非同期処理を行い、その結果を待って次のステップに進みたい場合は、必ずその非同期処理が返すPromiseをreturnしてください。

  4. Unhandled Rejectionへの対応:
    開発環境でUnhandled Rejectionが発生した場合、多くの場合コンソールに警告が表示されますが、プロダクション環境では見落とされがちで、場合によってはアプリケーションのクラッシュにつながります。
    対策: ブラウザ環境ではwindow.addEventListener('unhandledrejection', ...)、Node.js環境ではprocess.on('unhandledRejection', ...)を使って、捕捉されなかったPromiseの拒否をグローバルにハンドリングし、ログ記録サービスに報告するなど、原因究明のための仕組みを導入しましょう。ただし、根本的な解決策は、コード内で適切に.catch()を使ってエラーを捕捉することです。

  5. デバッグ手法:
    Promiseチェーンやasync/awaitを使った非同期処理は、同期処理に比べて処理の流れが複雑になるため、デバッグが難しく感じることがあります。
    対策:

    • コンソールログ: .then().catch()のコールバックの先頭でコンソールログを出力し、どのステップが実行されたか、どのような値が渡されたかを確認します。
    • 開発者ツール: ブラウザの開発者ツール(ChromeのSourcesタブなど)では、非同期コールスタックを表示できる機能があります。これにより、非同期処理がどのように呼び出し元に戻るかを視覚的に追跡できます。また、多くの開発者ツールにはPromiseインスペクタのような機能があり、Promiseの状態遷移を確認できます。
    • エラーメッセージの確認: エラーが発生した場合は、エラーメッセージだけでなく、スタックトレースを注意深く確認しましょう。Promiseチェーンの場合、エラーが発生した元の非同期処理や、エラーが伝播してきた.then()の場所を特定するのに役立ちます。
    • Promiseチェーンを分割: 長すぎるPromiseチェーンは、デバッグだけでなく可読性も低下させます。意味のあるまとまりごとに非同期関数として抽出し、短いチェーンやasync関数に分割することを検討しましょう。

Promiseベースの非同期処理に慣れるまでは、意図しない挙動に遭遇することもあるかもしれません。しかし、Promiseの基本原則(状態遷移、.then()の戻り値)、Promiseチェーンでの値とエラーの伝播ルールをしっかりと理解すれば、これらの落とし穴を避け、効率的にデバッグを進めることができるようになります。

まとめ:Promiseと.then()がもたらす非同期処理の未来

この記事では、JavaScriptにおける非同期処理の重要性から始まり、Callback Hellという課題を経て、Promiseがどのようにその問題を解決するために生まれたのかを解説しました。そして、Promiseの中心的な役割を担う.then()メソッドについて、その基本的な使い方から、新しいPromiseを返すという重要な特性、Promiseチェーンの構築、エラーハンドリング(.catch(), .finally())、そしてPromise.all()などの関連メソッドとの連携、さらにはasync/awaitとの関係性まで、約5000語にわたって徹底的に掘り下げてきました。

Promiseと.then()を理解し、使いこなすことの重要性を改めてまとめます。

  • Callback Hellからの脱却: Promiseチェーンを使うことで、ネストが深くなるCallback Hellを避け、非同期処理の流れを上から下へ、より直線的で読みやすいコードで記述できます。
  • 非同期処理の状態の標準化: Promiseは非同期操作の「未来の結果」をPending, Fulfilled, Rejectedという標準的な状態として表現します。これにより、様々な非同期APIや自作の非同期処理を、Promiseという統一的なインターフェースで扱うことができるようになります。
  • 堅牢なエラーハンドリング: .catch()メソッドを利用することで、非同期チェーン全体を通してエラーを一元的に捕捉し、適切に処理することが容易になります。これにより、アプリケーションの信頼性が向上します。
  • 柔軟な処理の連携: .then()のコールバックの戻り値のルールを理解することで、前の非同期処理の結果を使って次の処理を開始したり、異なる非同期処理を組み合わせたりといった複雑なワークフローを柔軟に構築できます。
  • async/awaitの基盤: async/awaitはPromiseのシンタックスシュガーであり、Promiseと.then()の理解がasync/awaitの効果的な利用とデバッグに不可欠です。

現代のJavaScript開発において、Promiseは非同期処理を扱う上で欠かせない存在です。Fetch API、新しいNode.jsの非同期API、多くのサードパーティライブラリなどがPromiseベースで設計されています。Promiseと.then()の知識は、これらのAPIやライブラリを使いこなすための必須スキルと言えるでしょう。

この記事を通じて、Promiseの概念、特に.then()メソッドの挙動とPromiseチェーンの仕組みについて、深く理解していただけたなら幸いです。非同期処理はJavaScriptの強力な側面の1つであり、Promiseと.then()はその力を安全かつ効率的に引き出すための鍵となります。

Promiseの学習はこれで終わりではありません。async/awaitをさらに深く学び、Promiseを返す非同期関数を自分で設計できるようになることで、JavaScriptの非同期処理マスターへの道は開かれていきます。

さあ、習得した知識を活かして、非同期処理を使ったモダンなJavaScript開発を楽しんでください!


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール