JavaScript .then() メソッドとは?使い方を徹底解説

はい、承知いたしました。JavaScriptの.then()メソッドについて、詳細な解説を含む約5000語の記事を記述し、ここに直接表示します。


JavaScript .then() メソッドとは?使い方を徹底解説

はじめに:非同期処理とPromiseの必要性

現代のWebアプリケーションやサーバーサイドアプリケーションにおいて、非同期処理は不可欠な要素です。ネットワークからのデータ取得、ファイルシステムの操作、データベースへのアクセス、タイマー処理など、多くの処理は完了までに時間がかかり、その間プログラムの実行をブロックしてしまうと、ユーザーインターフェリエンスの悪化やサーバーのスケーラビリティの問題を引き起こします。

古くから、JavaScriptでは非同期処理の結果を扱うために「コールバック関数」が広く使われてきました。例えば、XMLHttpRequestを使ってサーバーからデータを取得し、その結果を使って別の処理を行う場合、データ取得完了時に呼び出されるコールバック関数に関心のある処理を記述しました。

javascript
// 従来のコールバックを使った例 (擬似コード)
getData('/api/data', function(error, data) {
if (error) {
console.error('データの取得に失敗しました:', error);
} else {
console.log('データを受信しました:', data);
// 受信したデータを使って別の非同期処理を開始
processData(data, function(error, result) {
if (error) {
console.error('データの処理に失敗しました:', error);
} else {
console.log('データの処理が完了しました:', result);
// さらに別の非同期処理...
saveResult(result, function(error) {
if (error) {
console.error('結果の保存に失敗しました:', error);
} else {
console.log('結果が保存されました。');
}
});
}
});
}
});

このコードは、非同期処理が複数連続する場合に問題を引き起こします。一つの非同期処理の完了を待って次の非同期処理を開始し、その結果を待ってさらに次の処理を行う、といった連鎖が発生すると、コードはどんどんネストが深くなり、可読性が著しく低下します。これは「コールバック地獄(Callback Hell)」、あるいは「破滅のピラミッド(Pyramid of Doom)」と呼ばれ、JavaScript開発者にとって長年の悩みでした。

このような問題を解決するために、JavaScriptに標準で導入されたのが「Promise」オブジェクトです。Promiseは、非同期操作の「最終的な完了(または失敗)」とその「結果の値」を表すオブジェクトです。非同期処理が完了した未来の状態に対する「約束(Promise)」と考えることができます。

Promiseを使うことで、非同期処理の成功時と失敗時の処理を明確に分離し、複数の非同期処理をより分かりやすく、直感的に記述できるようになります。そして、そのPromiseオブジェクトの状態変化(成功または失敗)に対応して実行される処理を登録するための中心的なメソッドが、これから詳しく解説する.then()メソッドです。

この記事では、.then()メソッドの基本的な使い方から、Promiseチェーンによる複雑な非同期処理の連携、エラーハンドリング、さらにはasync/awaitとの関連まで、徹底的に解説していきます。.then()をマスターすることは、現代JavaScriptにおける非同期処理を理解する上で避けて通れない道です。

非同期処理とは?同期処理との違い

.then()メソッドを理解する前提として、非同期処理とは何か、そしてそれが同期処理とどう違うのかを改めて確認しましょう。

同期処理(Synchronous)

同期処理は、タスクが順番に実行される処理モデルです。あるタスクが完了するまで、次のタスクは開始されません。

javascript
console.log('タスク1を開始');
// 時間のかかる同期処理 (例: CPUを大量に消費する計算など)
function calculateSync() {
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
return sum;
}
const result = calculateSync();
console.log('タスク1が完了しました。結果:', result);
console.log('タスク2を開始');
// ...

この例では、calculateSync()関数が実行されている間、JavaScriptエンジンは他の処理(例えばUIの更新など)を一切行うことができません。処理が完了するまで「ブロック」されます。ブラウザ環境であれば、ページの応答性が悪化し、ユーザーインターフェースが固まってしまう原因となります。Node.jsのようなサーバー環境であれば、他のリクエストを処理できなくなり、スループットが低下します。

非同期処理(Asynchronous)

非同期処理は、タスクの完了を待たずに次のタスクに進む処理モデルです。時間のかかるタスクはバックグラウンドや別のメカニズムに委ねられ、完了した時点で登録しておいた処理(コールバック関数など)が実行されます。

javascript
console.log('タスクAを開始');
// 非同期処理 (例: サーバーからのデータ取得をシミュレート)
setTimeout(function() {
console.log('非同期タスクが完了しました。');
}, 2000); // 2秒後に実行される
console.log('タスクBを開始');
// ... 他の処理

この例では、setTimeoutが呼び出された後、JavaScriptエンジンは2秒間待つことなくすぐに次の行であるconsole.log('タスクBを開始');を実行します。非同期タスク(setTimeoutに渡された関数)は、2秒後にイベントループによって実行されます。これにより、時間のかかる処理の間も、他の処理を進めることができます。

JavaScriptにおいて非同期処理が必要な代表的な場面は以下の通りです。

  • ネットワーク通信(APIからのデータ取得、ファイルのアップロード/ダウンロードなど)
  • ファイルシステム操作(ファイルの読み書きなど)
  • タイマー(setTimeout, setInterval
  • データベース操作
  • ユーザーインタラクション(クリックイベントなど、発生を待つ処理)

これらの処理は、いつ完了するかが予測できない、あるいは完了までに時間がかかるため、同期的に実行するとプログラム全体が停止してしまいます。非同期処理は、このようなボトルネックを解消し、効率的で応答性の高いアプリケーションを構築するために不可欠です。

しかし、前述のコールバック地獄のように、非同期処理の結果を扱う方法が複雑になるという課題がありました。この課題を解決するために登場したのがPromiseであり、その結果の受け取り方を提供するのが.then()メソッドなのです。

Promiseとは?非同期操作の状態管理

.then()メソッドを深く理解するためには、Promiseオブジェクト自体について詳しく知る必要があります。

Promiseの定義

Promiseは、非同期操作の最終的な完了(または失敗)とその結果の値を表現するオブジェクトです。一言で言えば、「将来のある時点で利用可能になるかもしれない値」のプレースホルダーです。

Promiseの3つの状態

Promiseオブジェクトは、そのライフサイクルにおいて以下の3つのいずれかの状態をとります。

  1. pending (保留): 非同期操作がまだ完了していない初期状態。Promiseはまだ成功も失敗もしていません。
  2. fulfilled (成功): 非同期操作が成功裏に完了した状態。この状態になると、Promiseは「成功値」を持ちます。
  3. rejected (失敗): 非同期操作がエラーなどで失敗した状態。この状態になると、Promiseは「拒否理由」(通常はErrorオブジェクト)を持ちます。

Promiseは、pending状態から一度だけ、fulfilled状態またはrejected状態のいずれかに遷移します。一度状態がfulfilledまたはrejectedになると、それ以降その状態は変化しません。この状態変化は「解決(settled)」と呼ばれます。settled状態には、fulfilledとrejectedの両方が含まれます。

Promiseオブジェクトの作成

自分でPromiseを作成するには、new Promise()コンストラクタを使用します。コンストラクタには「Executor関数」と呼ばれる関数を引数として渡します。

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

const success = true; // 例として成功/失敗を切り替えるフラグ

if (success) {
// 成功の場合、resolve() を呼び出し、成功値を渡す
const result = “非同期処理が成功しました!”;
resolve(result);
} else {
// 失敗の場合、reject() を呼び出し、拒否理由(エラー)を渡す
const error = new Error(“非同期処理が失敗しました。”);
reject(error);
}
});
“`

Executor関数は、JavaScriptエンジンによってPromiseコンストラクタが実行された直後に、同期的に呼び出されます。このExecutor関数は、resolverejectという2つの関数を引数として受け取ります。

  • resolve(value): 非同期操作が成功したときに呼び出します。Promiseの状態をpendingからfulfilledに変更し、valueをそのPromiseの成功値とします。
  • reject(reason): 非同期操作が失敗したときに呼び出します。Promiseの状態をpendingからrejectedに変更し、reasonをそのPromiseの拒否理由とします。通常、reasonErrorオブジェクトまたはそのサブクラスのインスタンスです。

Executor関数内で発生した同期的なエラー(例外)は、自動的にPromiseをrejected状態にします。rejectを明示的に呼び出す必要はありません。

“`javascript
const promiseWithError = new Promise((resolve, reject) => {
// 同期的なエラーを発生させる
throw new Error(“Executor関数内で同期エラーが発生しました!”);
// resolve() や reject() は呼ばれない
});

promiseWithError.catch(error => {
console.error(“Promiseがrejectedされました:”, error); // この行が実行される
});
“`

Promiseの状態遷移と値/理由の保持という仕組みにより、非同期処理の「完了」という概念をオブジェクトとして表現し、その結果を後から取得したり、結果に応じた処理を登録したりすることが可能になります。そして、この「結果に応じた処理の登録」を行うのが、.then()メソッドの役割です。

.then() メソッドの基本

.then()メソッドは、Promiseオブジェクトにアタッチして使用します。その主な役割は、Promiseが解決(fulfilledまたはrejected)したときに実行されるコールバック関数を登録することです。

Promiseが解決される前(pending状態のとき)に.then()を呼び出しても、登録されたコールバック関数はすぐに実行されません。Promiseが解決された時点で、登録しておいた適切なコールバック関数が非同期的に実行されます。もし.then()が呼び出された時点でPromiseが既に解決済み(fulfilledまたはrejected)であれば、コールバック関数は(それでもやはり非同期的に)すぐに実行されます。

.then() メソッドの構文

.then()メソッドは、最大で2つの引数を取ります。どちらの引数も関数です。

javascript
promise.then(onFulfilled, onRejected);

  • onFulfilled: Promiseがfulfilled(成功)状態になったときに実行されるコールバック関数です。成功値を引数として受け取ります。この引数は省略可能です。
  • onRejected: Promiseがrejected(失敗)状態になったときに実行されるコールバック関数です。拒否理由(通常はエラーオブジェクト)を引数として受け取ります。この引数も省略可能です。

onFulfilled コールバック関数

onFulfilled関数は、Promiseが成功したときに呼び出されます。Promiseがfulfilled状態になった際の成功値が、この関数の引数として渡されます。

javascript
myPromise.then(
function(value) {
// Promiseが成功したときに実行される
console.log("成功しました! 値:", value); // "成功しました! 値: 非同期処理が成功しました!"
}
// onRejected は省略
);

Promiseが成功した場合、このonFulfilled関数内の処理が実行されます。

onRejected コールバック関数

onRejected関数は、Promiseが失敗したときに呼び出されます。Promiseがrejected状態になった際の拒否理由が、この関数の引数として渡されます。

javascript
// myPromise が rejected になった場合
myPromise.then(
// onFulfilled は省略
null, // または undefined
function(reason) {
// Promiseが失敗したときに実行される
console.error("失敗しました! 理由:", reason); // "失敗しました! 理由: Error: 非同期処理が失敗しました。"
}
);

Promiseが失敗した場合、このonRejected関数内の処理が実行されます。エラーハンドリングのために非常に重要です。

onRejectedコールバック関数を省略した場合、Promiseがrejected状態になっても.then()はそのエラーを捕捉しません。エラーは次に.then(onFulfilled, onRejected)onRejected引数が指定されている.then()、または.catch()メソッドが呼び出されるまで、Promiseチェーンを下流に伝播していきます。もしチェーンの最後までエラーハンドラーが見つからなかった場合、そのエラーは捕捉されずに未処理のPromise拒否エラーとして扱われ、環境によってはアプリケーションがクラッシュしたり、エラーメッセージが表示されたりします。

.then() メソッドの戻り値:Promiseチェーンの鍵

.then()メソッドの最も重要で強力な特性の一つは、常に新しいPromiseオブジェクトを返すことです。

“`javascript
const firstPromise = new Promise((resolve) => resolve(“Hello”));

const secondPromise = firstPromise.then(value => {
console.log(value); // “Hello”
return “World”; // 新しいPromiseの成功値になる値
});

secondPromise.then(value => {
console.log(value); // “World”
});
“`

この「新しいPromiseを返す」という性質こそが、Promiseチェーンを可能にし、コールバック地獄から私たちを解放する鍵となります。.then()メソッドから返される新しいPromiseは、.then()に渡されたonFulfilledまたはonRejectedコールバック関数の戻り値によって、その状態と値が決定されます。これについては後述の「Promiseチェーン」のセクションで詳しく解説します。

補足: コールバックは非同期に実行される

重要な注意点として、.then()に登録されたonFulfilledonRejectedといったコールバック関数は、たとえPromiseが.then()呼び出し時点で既に解決済みであったとしても、必ず非同期的に実行されます。これは、JavaScriptのイベントループのマイクロタスクキューの仕組みによるものです。.then()のコールバックは、現在の同期的なコードブロックの実行が完了した後、他の保留中のマイクロタスクが実行された後に実行されます。

“`javascript
const resolvedPromise = Promise.resolve(“すぐに解決するPromise”);

resolvedPromise.then(value => {
console.log(value); // (3) 実行される
});

console.log(“これはすぐに表示されます”); // (1) 実行される

// 非同期的なタスクの実行順序を視覚化
// (1) ‘これはすぐに表示されます’ が同期的にログ出力される
// (2) resolvedPromise は既に解決済みだが、.then() のコールバックはマイクロタスクとしてキューに追加される
// (3) 現在の同期処理が完了した後、マイクロタスクキューにある .then() のコールバックが実行される
“`

この非同期実行の性質は、同期的なエラーハンドリング(try...catch)とPromiseのエラーハンドリング(.then(null, onRejected).catch())の挙動の違いを理解する上で重要になります。

.then() メソッドの具体的な使い方 (基本編)

いくつかの簡単な例を通じて、.then()メソッドの基本的な使い方を確認しましょう。

成功時の処理だけを記述する場合

Promiseが成功した場合の処理だけに関心がある場合は、onFulfilledコールバックだけを渡します。onRejectedは省略できます。

“`javascript
// 成功するPromiseを作成
const successPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(“データ取得成功!”);
}, 1000);
});

// 成功時の処理だけを登録
successPromise.then(value => {
console.log(“成功:”, value); // 1秒後に ‘成功: データ取得成功!’ と表示される
});
“`

この場合、もしsuccessPromiseがrejectedになったとしても、この.then()は何もせず、エラーは次の.then().catch()に伝播します。

成功時と失敗時の両方を記述する場合

Promiseが成功した場合と失敗した場合の両方の処理を、一つの.then()で記述することもできます。

“`javascript
// 失敗するPromiseを作成
const failurePromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error(“ネットワークエラー”));
}, 1000);
});

// 成功時と失敗時の両方の処理を登録
failurePromise.then(
value => {
// この Promise は rejected になるため、この onFulfilled は実行されない
console.log(“成功 (これは表示されないはず):”, value);
},
reason => {
// Promise が失敗したときに実行される
console.error(“失敗:”, reason.message); // 1秒後に ‘失敗: ネットワークエラー’ と表示される
}
);
“`

このように、.then()の第二引数としてonRejectedコールバック関数を渡すことで、エラーハンドリングを行うことができます。

ただし、後述する.catch()メソッドを使った方が、コードの可読性やPromiseチェーン全体のエラーハンドリングの観点から推奨されることが多いです。

非同期操作の例: setTimeout を Promise 化する

実際の非同期処理をPromiseと.then()で扱う簡単な例として、setTimeoutをPromiseでラップしてみましょう。

``javascript
function delay(ms) {
return new Promise(resolve => {
setTimeout(() => {
resolve(
指定した ${ms} ミリ秒が経過しました`);
}, ms);
});
}

console.log(“遅延を開始…”);

delay(2000).then(message => {
console.log(message); // 2秒後に ‘指定した 2000 ミリ秒が経過しました’ と表示される
});

console.log(“遅延待たずに次の処理へ”); // こちらはすぐに表示される
“`

この例では、delay関数がPromiseを返します。このPromiseは、指定されたミリ秒が経過した後にresolveされます。.then()メソッドを使って、Promiseがresolveされたときの処理(メッセージのログ出力)を登録しています。コードの実行順序は、非同期処理の性質を示しています。

非同期操作の例: Fetch API と .then()

Web開発でよく使われるFetch APIは、Promiseを返します。これと.then()を組み合わせてHTTPリクエストの結果を扱うのが典型的なパターンです。

``javascript
// 存在しないURLへのリクエスト (失敗例)
fetch('https://jsonplaceholder.typicode.com/invalid-url')
.then(response => {
// HTTPステータスコードが200番台であれば成功とみなされる
// ただし、404 Not Foundなども fulfilled になることに注意
if (!response.ok) {
// response.ok が false の場合は、HTTPエラー (404, 500など)
// エラーとして扱うために Promise を reject する
throw new Error(
HTTP error! status: ${response.status}`);
}
// レスポンスボディをJSONとしてパースする (これも非同期処理でありPromiseを返す)
return response.json();
})
.then(data => {
// レスポンスボディのパースが成功した場合
console.log(‘データ取得成功:’, data);
})
.catch(error => {
// 上記の .then() チェーンのどこかで発生したエラー(ネットワークエラー、HTTPエラー、JSONパースエラーなど)を捕捉
console.error(‘データ取得失敗:’, error);
});

// 存在するURLへのリクエスト (成功例)
fetch(‘https://jsonplaceholder.typicode.com/todos/1’)
.then(response => {
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
return response.json();
})
.then(data => {
console.log(‘TODOデータ取得成功:’, data);
})
.catch(error => {
console.error(‘TODOデータ取得失敗:’, error);
});
“`

この例は、Fetch APIが返すPromiseに対して.then()をチェーンさせている典型的なパターンです。最初の.then()ではHTTPレスポンスを受け取り、レスポンスが成功しているか確認し、次の非同期処理であるresponse.json()(レスポンスボディをJSONとして読み込む)を呼び出してそのPromiseを返しています。二番目の.then()は、response.json()が返したPromiseが解決された後の処理(JSONデータの利用)を行っています。.catch()は、これらの.then()のどの段階で発生したエラーもまとめて捕捉しています。

これは、.then()メソッドが新しいPromiseを返すことで実現される「Promiseチェーン」の強力さを示す良い例です。

.then() メソッドチェーン

Promiseと.then()メソッドの真価は、複数の非同期処理を順番に、あるいは連携して実行する際に発揮されます。これは、.then()メソッドが常に新しいPromiseを返すという性質を利用した「Promiseチェーン」によって実現されます。

チェーンの仕組み

promise.then(onFulfilled, onRejected) が新しいPromise newPromise を返すことを思い出してください。この newPromise の状態と値は、.then() に渡された onFulfilled または onRejected コールバック関数の戻り値によって決まります。

  1. コールバック関数が値を返した場合:
    onFulfilled または onRejected コールバック関数が値を返した場合、.then()から返される新しいPromiseは、その返された値でfulfilled状態になります。

    javascript
    Promise.resolve(1) // ① resolved with 1
    .then(value => { // ② onFulfilled executed with 1
    console.log(value); // 1
    return value + 1; // ③ return 2
    }) // ④ returns a new Promise, resolved with 2
    .then(value => { // ⑤ onFulfilled executed with 2
    console.log(value); // 2
    // (何も返さない場合は undefined が返されたとみなされる)
    }) // ⑥ returns a new Promise, resolved with undefined
    .then(value => { // ⑦ onFulfilled executed with undefined
    console.log(value); // undefined
    });

    この例では、最初の.then()onFulfilledvalue + 1(すなわち2)を返しています。その結果、その.then()から返されるPromiseは値2で解決され、次の.then()onFulfilledはその値2を受け取ります。

  2. コールバック関数がPromiseを返した場合:
    onFulfilled または onRejected コールバック関数がPromiseを返した場合、.then()から返される新しいPromiseは、その返されたPromiseと同じ状態と値/理由になります。つまり、次の.then()は、返されたPromiseが解決されるまで待機します。これが非同期処理の連鎖を実現するメカニズムです。

    “`javascript
    function asyncAdd(a, b) {
    return new Promise(resolve => {
    setTimeout(() => resolve(a + b), 1000);
    });
    }

    asyncAdd(1, 2) // ① Returns a Promise that resolves with 3 after 1s
    .then(result => { // ② onFulfilled executed after 1s with 3
    console.log(“最初の加算結果:”, result); // 最初の加算結果: 3
    return asyncAdd(result, 3); // ③ Returns a new Promise (asyncAdd(3, 3))
    }) // ④ Returns a new Promise that waits for asyncAdd(3, 3) to resolve (after another 1s)
    .then(result => { // ⑤ onFulfilled executed after another 1s with 6
    console.log(“次の加算結果:”, result); // 次の加算結果: 6
    return asyncAdd(result, 4); // ⑥ Returns a new Promise (asyncAdd(6, 4))
    }) // ⑦ Returns a new Promise that waits for asyncAdd(6, 4) to resolve (after another 1s)
    .then(result => { // ⑧ onFulfilled executed after another 1s with 10
    console.log(“最終結果:”, result); // 最終結果: 10
    });

    console.log(“チェーン開始”); // これは最初に表示される
    “`

    この例では、最初のasyncAdd(1, 2)が返すPromiseが解決した後、最初の.then()onFulfilledが実行されます。このコールバックは、次の非同期処理であるasyncAdd(result, 3)(結果はasyncAdd(3, 3))が返す新しいPromiseを返しています。したがって、最初の.then()から返されるPromiseは、asyncAdd(3, 3)が返すPromiseと同じ状態になります。つまり、次の.then()asyncAdd(3, 3)が解決されるまで待機します。このようにして、非同期処理が順番に実行されるチェーンが構築されます。

    このパターンが、コールバック地獄で深いネストを作っていた処理(例えば、ファイルAを読み込んで、その内容を使ってファイルBを読み込んで、その内容を使ってサーバーにPOSTするなど)を、フラットで読みやすいチェーンとして記述することを可能にします。

チェーン内でのエラーハンドリング

Promiseチェーンの途中でエラーが発生した場合、そのエラーはチェーンを下流に伝播していきます。次に.then(onFulfilled, onRejected)形式でonRejectedコールバックが指定されている.then()、または.catch()メソッドが見つかるまで、成功時のonFulfilledコールバックはスキップされます。

javascript
Promise.resolve("開始")
.then(value => {
console.log(value); // 開始
throw new Error("チェーン途中でエラー発生!"); // エラーを発生させる
// この行以降のコードは実行されない
return "これは返されない";
})
.then(value => {
// 上の then でエラーが発生したので、この onFulfilled はスキップされる
console.log("この then は実行されない:", value);
return value + " 次へ";
})
.then(value => {
// 同上、この onFulfilled もスキップされる
console.log("これも実行されない:", value);
})
.catch(error => {
// チェーンのどこかで発生した最初のエラーを捕捉する
console.error("エラー捕捉:", error.message); // エラー捕捉: チェーン途中でエラー発生!
// .catch() ブロックも Promise を返す。何も返さない場合は resolved with undefined
// 明示的に値を返すと resolved with その値 になる
// 明示的にエラーを throw すると rejected with そのエラー になる
return "エラーから回復";
})
.then(value => {
// .catch() がエラーを捕捉し、'エラーから回復' を返したので、この onFulfilled が実行される
console.log(".catch() の後で実行:", value); // .catch() の後で実行: エラーから回復
});

この例は、チェーン内でのエラー伝播と.catch()によるエラーハンドリングを示しています。最初の.then()で発生したエラーは、後続の成功時ハンドラーをスキップし、最も近いエラーハンドラーである.catch()に到達します。.catch()でエラーが捕捉され、新しいPromiseが返される(この例では"エラーから回復"という値で解決される)ことで、チェーンは成功パスに戻り、.catch()の後の.then()が実行されます。

Promiseチェーンの利点

Promiseチェーンを使用することで、以下のような利点が得られます。

  • 可読性の向上: 非同期処理のフローが上から下へ、直線的に記述されるため、コードの追跡が容易になります。
  • コールバック地獄の解消: ネストが深くなることなく、複数の非同期処理を順番に実行できます。
  • エラーハンドリングの一元化: .catch()メソッドを使うことで、チェーン全体のどこで発生したエラーもまとめて捕捉し、処理できます。
  • 状態管理の明確化: Promiseの状態(pending, fulfilled, rejected)が明確に定義されているため、現在の非同期操作がどの段階にあるのか、成功したのか失敗したのかを把握しやすくなります。

.then() と .catch()

前述の通り、.then()メソッドの第2引数としてonRejectedコールバック関数を渡すことでエラーハンドリングが可能ですが、Promiseチェーンにおいては.catch()メソッドを使うのがより一般的で推奨されます。

.catch() メソッドとは

.catch()メソッドは、promise.catch(onRejected) という構文で使用します。これは、実質的には promise.then(undefined, onRejected) の糖衣構文(Syntactic Sugar)です。つまり、.catch()は成功時のハンドラーを持たず、Promiseがrejected状態になったときだけ実行されるコールバック関数(onRejected)を登録するためのメソッドです。

“`javascript
myPromise.catch(reason => {
// Promise が rejected になったときに実行される
console.error(“エラーを捕捉しました:”, reason);
});

// これは以下のコードとほぼ同じ意味
myPromise.then(undefined, reason => {
console.error(“エラーを捕捉しました:”, reason);
});
“`

.catch() を使うメリット

  1. コードの可読性: 成功時の処理 (.then()) と失敗時の処理 (.catch()) が分離されるため、コードがより整理され、何が成功パスで何がエラーハンドリングなのかが一目で分かりやすくなります。
  2. チェーン全体の捕捉: .catch()をPromiseチェーンの最後に配置すると、そのチェーンのどの段階で発生したエラーもまとめて捕捉することができます。これは、.then()の第2引数では実現しにくい利点です(.then()の第2引数は、その直前のPromiseのエラーしか捕捉しないため)。

javascript
// チェーンの途中でエラーが発生する可能性のあるコード
doSomethingAsync() // Promise A を返す
.then(resultA => {
// Promise A が成功したら実行
return doSomethingElseAsync(resultA); // Promise B を返す
})
.then(resultB => {
// Promise B が成功したら実行
return doAnotherThingAsync(resultB); // Promise C を返す
})
.then(resultC => {
// Promise C が成功したら実行
console.log("すべての処理が成功:", resultC);
})
.catch(error => {
// Promise A, B, C のいずれかでエラーが発生した場合、
// または then コールバック内で同期的なエラーが発生した場合、
// この catch ブロックが実行される
console.error("チェーンのどこかでエラーが発生しました:", error);
});

この構造により、非同期フローを記述する本流(.then()チェーン)と、エラー発生時の横断的な処理(.catch())を明確に分けることができます。

.then(onFulfilled, onRejected).then(onFulfilled).catch(onRejected) の違い

一見似ていますが、これらには重要な違いがあります。

1. .then(onFulfilled, onRejected)

javascript
promise
.then(
value => {
// 成功時の処理
console.log("成功:", value);
// ここで発生したエラーは、この then の onRejected では捕捉できない!
throw new Error("成功ハンドラ内でエラー");
},
reason => {
// 失敗時の処理 (promise 自体の失敗を捕捉)
console.error("then の onRejected で捕捉:", reason);
// この then の onRejected から返される Promise の状態は、
// ここで throw されたエラーや return された値によって決まる
// エラーを throw すると、そのエラーが次のエラーハンドラに伝播する
throw reason;
}
)
.then(
value => {
console.log("次の then の成功:", value);
},
reason => {
console.error("次の then の onRejected で捕捉:", reason); // ここで上の then の成功ハンドラ内のエラーを捕捉できる
}
);

.then(onFulfilled, onRejected)の場合、onRejectedコールバックはその.then()がアタッチされているPromise自体がrejectedになった場合にのみ呼び出されます。onFulfilledコールバック関数内で発生したエラーは、その同じ.then()onRejectedでは捕捉されず、次の.then()のエラーハンドラーか、.catch()メソッドに伝播します。

2. .then(onFulfilled).catch(onRejected)

javascript
promise
.then(value => {
// 成功時の処理
console.log("成功:", value);
// ここで発生したエラーは、この then の後に続く catch で捕捉できる
throw new Error("成功ハンドラ内でエラー");
return "成功値";
}) // この then から返される Promise は、上の成功ハンドラ内でエラーが投げられたため rejected になる
.then(value => {
// 上の then が rejected になったため、この then の onFulfilled はスキップされる
console.log("次の then の成功 (スキップされる):", value);
}) // この then はスキップされたため、返される Promise も rejected のまま (エラーを伝播)
.catch(reason => {
// 上の then で発生したエラー、または promise 自体のエラーをまとめて捕捉
console.error(".catch で捕捉:", reason.message); // .catch で捕捉: 成功ハンドラ内でエラー
});

.then(onFulfilled).catch(onRejected)の場合、.catch()その直前の.then()メソッドから返されたPromiseに対してアタッチされます。その.then()から返されたPromiseは、以下のいずれかの理由でrejectedになる可能性があります。

  • 元のpromiseがrejectedだった場合(ただし、.then()の第1引数が省略されているか、その第1引数がエラーを再throwした場合)
  • .then()に渡されたonFulfilledコールバック関数内でエラーが発生した(またはエラーをthrowした)場合
  • .then()に渡されたonFulfilledコールバック関数がrejectedなPromiseを返した場合

したがって、.then().catch()の形式は、直前の成功ハンドラ内で発生したエラーも.catch()でまとめて捕捉できるため、エラーハンドリングの範囲が広く、より安全で推奨されるパターンです。通常、Promiseチェーンの最後には単一の.catch()ブロックを配置し、チェーン全体のエラーを一括して処理します。

.then() メソッドの応用

Promiseと.then()の組み合わせは、単に一つの非同期処理の結果を待つだけでなく、より複雑な非同期フローを制御するためにも使用できます。

複数の非同期処理の連携

直列処理

前の処理の結果を使って次の処理を開始する場合、Promiseチェーンが最適です。

“`javascript
// 例: ユーザーIDを取得 -> そのIDを使ってユーザー詳細を取得 -> そのユーザーの詳細を使って記事一覧を取得

function getUserId() {
return new Promise(resolve => setTimeout(() => resolve(123), 500));
}

function getUserDetails(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === 123) {
resolve({ name: “Alice”, city: “Tokyo” });
} else {
reject(new Error(“Unknown user ID”));
}
}, 800);
});
}

function getUserArticles(userDetails) {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{ title: “Article 1”, author: userDetails.name },
{ title: “Article 2”, author: userDetails.name }
]);
}, 700);
});
}

getUserId()
.then(userId => {
console.log(“ユーザーID取得:”, userId);
return getUserDetails(userId); // 次の then に Promise を渡す
})
.then(userDetails => {
console.log(“ユーザー詳細取得:”, userDetails);
return getUserArticles(userDetails); // 次の then に Promise を渡す
})
.then(articles => {
console.log(“記事一覧取得:”, articles);
console.log(“全ての処理が完了しました!”);
})
.catch(error => {
console.error(“処理中にエラーが発生:”, error);
});

console.log(“処理開始…”);
// 出力順序:
// 処理開始…
// ユーザーID取得: 123 (約0.5秒後)
// ユーザー詳細取得: { name: ‘Alice’, city: ‘Tokyo’ } (さらに約0.8秒後)
// 記事一覧取得: [ { title: ‘Article 1’, author: ‘Alice’ }, { title: ‘Article 2’, author: ‘Alice’ } ] (さらに約0.7秒後)
// 全ての処理が完了しました!
“`

この例では、各.then()コールバック関数が、次の非同期処理を呼び出し、その結果として得られるPromiseを返しています。これにより、非同期処理が順番に実行されることが保証されます。

並列処理

複数の非同期処理を並行して実行し、全ての完了を待つ、または最初に完了したものの結果を利用したい場合があります。この用途のために、Promiseにはいくつかの静的メソッドが用意されています。これらのメソッドも、Promiseを返し、.then()と組み合わせて使用します。

  • Promise.all(iterable): iterable(通常はPromiseオブジェクトの配列)内の全てのPromiseがfulfilledになるのを待ちます。全てのPromiseが成功した場合、それらの成功値を元のPromiseの順番で格納した配列で解決されるPromiseを返します。一つでもrejectedになった場合、その最初のエラー理由で即座にrejectedになるPromiseを返します。
  • Promise.race(iterable): iterable内のPromiseのうち、どれか一つでも最初に解決(fulfilledまたはrejected)されるのを待ちます。最初に解決したPromiseと同じ状態と値/理由で解決されるPromiseを返します。
  • Promise.allSettled(iterable): iterable内の全てのPromiseが解決(fulfilledまたはrejectedのいずれか)されるのを待ちます。それぞれのPromiseの結果(成功または失敗、およびその値/理由)を記述したオブジェクトの配列で解決されるPromiseを返します。個々のPromiseが失敗しても、このメソッド自体が返すPromiseはrejectedになりません。
  • Promise.any(iterable): iterable内のPromiseのうち、どれか一つでも最初にfulfilledになるのを待ちます。最初に成功したPromiseの成功値で解決されるPromiseを返します。全てのPromiseがrejectedになった場合、個々のエラー理由をまとめたAggregateErrorでrejectedになるPromiseを返します。

これらのメソッドが返すPromiseに対して.then().catch()を使用します。

“`javascript
// 例: 複数のURLからデータを並列取得し、全ての結果を待つ

const urls = [
‘https://jsonplaceholder.typicode.com/todos/1’,
‘https://jsonplaceholder.typicode.com/posts/1’,
‘https://jsonplaceholder.typicode.com/users/1’
];

// FetchリクエストのPromise配列を作成
const promises = urls.map(url =>
fetch(url).then(response => {
if (!response.ok) {
throw new Error(Failed to fetch ${url}: ${response.status});
}
return response.json();
})
);

// 全てのPromiseが完了するのを待つ
Promise.all(promises)
.then(results => {
// results は、各 fetch が返した Promise が成功した値の配列
console.log(“全てのデータ取得成功:”, results);
// results[0] は todos/1 のデータ
// results[1] は posts/1 のデータ
// results[2] は users/1 のデータ
})
.catch(error => {
// いずれかの fetch が失敗した場合、最初のエラーを捕捉
console.error(“データの取得中にエラーが発生:”, error);
});
“`

この例では、urls.map()でFetchリクエストを行うPromiseの配列を作成し、Promise.all()に渡しています。Promise.all()が返すPromiseに対して.then()を呼び出すことで、全てのFetchリクエストが成功してJSONパースが完了した後に、その結果配列をまとめて処理しています。.catch()は、いずれかのリクエストが失敗した場合にそのエラーを捕捉します。

.then() メソッド使用上の注意点

Promiseと.then()は強力ですが、使用上の注意点もいくつかあります。

コールバック関数は常に非同期

前述の通り、.then()のコールバック関数は、たとえPromiseが解決済みであっても、現在の同期処理の実行が完了するまで待機し、マイクロタスクキューを介して非同期的に実行されます。この動作はPromiseの仕様で定められており、予期しない同期的な実行順序を防ぎ、一貫性のある非同期動作を提供するためのものです。

“`javascript
const p = Promise.resolve(“完了”);

p.then(value => {
console.log(value); // (2)
});

console.log(“直後の同期処理”); // (1)
“`

このコードは必ず “(1) 直後の同期処理” の後に “(2) 完了” が出力されます。

同期的なエラーの取り扱い

Executor関数内や.then().catch().finally()コールバック内で発生した同期的なエラー(例外)は、Promiseの仕組みによって自動的に捕捉され、そのPromiseをrejected状態にします。これにより、非同期処理だけでなく、その処理を行う関数内で発生した同期エラーもPromiseチェーンで一貫して扱えるようになります。

javascript
Promise.resolve("開始")
.then(value => {
console.log(value);
// 同期エラーを発生させる
throw new Error("同期的なエラー!");
})
.catch(error => {
console.error("エラー捕捉:", error.message); // エラー捕捉: 同期的なエラー!
});

ただし、非同期処理関数がPromiseを返さず、例外をthrowするような従来のスタイルと混在する場合、注意が必要です。

“`javascript
function mightFailSyncOrAsync(shouldFailSync) {
if (shouldFailSync) {
// 同期的に失敗する場合
throw new Error(“同期的に失敗しました!”);
} else {
// 非同期的に成功する場合
return Promise.resolve(“非同期的に成功しました!”);
}
}

try {
// 同期的に失敗するケース
mightFailSyncOrAsync(true)
.then(result => console.log(“成功:”, result))
.catch(error => console.error(“Promise エラー捕捉:”, error)); // ここには来ない (同期エラーは try…catch で捕捉される)
} catch (error) {
console.error(“Try…catch で捕捉:”, error.message); // Try…catch で捕捉: 同期的に失敗しました!
}

// 非同期的に成功するケース
mightFailSyncOrAsync(false)
.then(result => console.log(“成功:”, result)) // 成功: 非同期的に成功しました!
.catch(error => console.error(“Promise エラー捕捉:”, error));
“`

Promiseチェーンの中で同期エラーが発生した場合、Promiseのメカニズムがエラーを捕捉してrejected Promiseに変換します。しかし、Promiseチェーンの外側で同期エラーが発生した場合は、従来のtry...catchブロックで捕捉する必要があります。非同期処理を行う関数は一貫してPromiseを返すように設計するのがベストプラクティスです。

エラーを取りこぼさない

Promiseチェーンにおいて、エラーを取りこぼさないためには、チェーンの最後に必ず.catch()メソッドを付けることが非常に重要です。

“`javascript
// Bad practice: Errors might be unhandled
doSomething()
.then(result => doSomethingElse(result))
.then(finalResult => console.log(“結果:”, finalResult));
// Error occurred in doSomethingElse or its then handler will not be caught here!

// Good practice: Add .catch() at the end
doSomething()
.then(result => doSomethingElse(result))
.then(finalResult => console.log(“結果:”, finalResult))
.catch(error => console.error(“エラー:”, error)); // Catches any error in the chain
“`

特にブラウザ環境などでは、未処理のPromise拒否はグローバルなunhandledrejectionイベントとして捕捉できますが、サーバーサイド(Node.js)などでは、未処理の拒否がアプリケーションクラッシュの原因となる場合があります。常に.catch()でエラーハンドリングを行う習慣をつけましょう。

コールバック内でPromiseチェーンを返さないミス

.then()のコールバック内でPromiseを返す代わりに、そのPromiseに対してさらに.then()を付けてしまい、その結果を返さないというミスをすることがあります。

“`javascript
// 間違いの例
asyncOperation1()
.then(result1 => {
// asyncOperation2 の結果を待たずに、次の then が実行されてしまう
asyncOperation2(result1).then(result2 => {
console.log(“結果2:”, result2);
});
// ここで何も返していない (暗黙的に undefined を返している)
})
.then(resultOfUndefined => {
// 上の then からは undefined が返ってきたので、これが実行されてしまう
console.log(“結果1の次に実行されるが、意図した順序ではない:”, resultOfUndefined); // resultOfUndefined は undefined
});

// 正しい例
asyncOperation1()
.then(result1 => {
// asyncOperation2 が返す Promise を返す
return asyncOperation2(result1);
})
.then(result2 => {
// asyncOperation2 の Promise が解決された後に、この then が実行される
console.log(“結果2:”, result2);
});
“`

Promiseチェーンを構築する場合、.then().catch()のコールバック関数からは、次のステップの結果を表すPromise(または単なる値)をreturnキーワードを使って返す必要があります。返されたPromiseの状態に基づいて、次の.then()が待機するか、または値を受け取って実行されるかが決定されます。

async/await と .then()

ECMAScript 2017で導入されたasync/await構文は、Promiseベースの非同期処理を、より同期的なコードに近い見た目で記述するための糖衣構文です。async/awaitはPromiseの上に構築されており、その内部では.then().catch()のようなPromiseのメカニズムが利用されています。

async/await の基本

  • asyncキーワードは関数宣言や関数式、メソッドの前に付け、その関数が非同期関数であることを示します。非同期関数は常にPromiseを返します。
  • awaitキーワードは、async関数内でのみ使用できます。awaitはPromiseの前に付け、そのPromiseが解決されるまで非同期的に待機します。Promiseがfulfilledされればその成功値を返し、rejectedされればエラーをthrowします。

``javascript
async function fetchData(url) {
try {
const response = await fetch(url); // fetch が返す Promise が解決されるまで待機
if (!response.ok) {
throw new Error(
HTTP error! status: ${response.status}`);
}
const data = await response.json(); // response.json() が返す Promise が解決されるまで待機
return data; // 非同期関数は値を返すと、その値で解決される Promise を返す
} catch (error) {
console.error(“エラー:”, error);
throw error; // エラーを throw すると、非同期関数が返す Promise は rejected になる
}
}

// async 関数の呼び出しは Promise を返すので、.then() や .catch() で結果を受け取れる
fetchData(‘https://jsonplaceholder.typicode.com/todos/1’)
.then(data => console.log(“取得データ:”, data))
.catch(error => console.error(“エラー捕捉:”, error));

// または、別の async 関数内で await する
async function processData() {
try {
const data = await fetchData(‘https://jsonplaceholder.typicode.com/todos/2’);
console.log(“別の場所で取得データ:”, data);
} catch (error) {
console.error(“別の場所でエラー捕捉:”, error);
}
}
processData();
“`

async/await と .then() の関係

await promise という構文は、概念的には以下のような.then()を使ったコードとほぼ等価です(ただし、エラーハンドリングの方法やスタックトレースなど、厳密にはいくつかの違いがあります)。

“`javascript
// await promise は概念的に以下のよう
// const result = await somePromise;

// これは概念的に以下と似ている
let result;
somePromise.then(value => {
result = value;
// await の後の処理を再開する (内部的な仕組み)
}).catch(error => {
// エラーを throw する (内部的な仕組み)
throw error;
});
// await はこの then/catch が完了するまで待機する
“`

非同期関数内でawaitに出会うと、関数の実行は一時停止され、そのPromiseが解決されるまで待機します。Promiseが解決されると、待機していた関数の実行が再開されます。これは、Promiseの.then()コールバックが実行されるタイミングと似ています。

async関数全体はPromiseを返します。このPromiseは、async関数が通常通り完了した場合は返された値でfulfilledされ、async関数内でエラーがthrowされた場合はそのエラー理由でrejectedされます。したがって、async関数を呼び出した側は、返されたPromiseに対して.then().catch()を使用して結果やエラーを受け取ることができます。

“`javascript
async function myFunction() {
// … async 処理 …
return “完了した値”; // この値で myFunction() が返す Promise が解決される
}

async function myFailedFunction() {
// … async 処理 …
throw new Error(“失敗しました”); // このエラーで myFailedFunction() が返す Promise が rejected になる
}

myFunction().then(value => console.log(“成功:”, value)); // 成功: 完了した値
myFailedFunction().catch(error => console.error(“失敗:”, error.message)); // 失敗: 失敗しました
“`

エラーハンドリングにおいて、async/awaitでは.catch()の代わりにtry...catchブロックがよく使われます。async関数内のtry...catchブロックは、そのブロック内で発生した同期エラーだけでなく、awaitしているPromiseがrejectedになった場合のエラーも捕捉します。これは、Promiseチェーンの最後に配置した.catch()がチェーン全体のエラーを捕捉するのと似た役割を果たします。

どちらを使うべきか?

async/awaitはPromiseベースのコードをより同期的に見えるようにし、複雑なチェーンや条件分岐を含む非同期フローの可読性を大幅に向上させることが多いです。特に、複数の非同期処理の結果を順番に利用していくようなケースでは、async/awaitの方が記述がシンプルになる傾向があります。

“`javascript
// Promise チェーンの例 (再掲)
getUserId()
.then(userId => getUserDetails(userId))
.then(userDetails => getUserArticles(userDetails))
.then(articles => console.log(articles))
.catch(error => console.error(error));

// async/await の例
async function fetchAllUserData() {
try {
const userId = await getUserId();
const userDetails = await getUserDetails(userId);
const articles = await getUserArticles(userDetails);
console.log(articles);
} catch (error) {
console.error(error);
}
}
fetchAllUserData();
“`

どちらのスタイルを使用するかは、プロジェクトのスタイルガイド、コードの複雑さ、個人の好みによって異なります。しかし、.then()async/awaitは相互排他的なものではなく、しばしば組み合わせて使用されます。例えば、非同期関数を呼び出し、その結果を.then().catch()で処理したり、Promise.all()と.then()を組み合わせて並列処理を行い、その結果をawaitしたりといった使い方が考えられます。

重要なのは、async/awaitがPromiseと.then()の概念の上に成り立っていることを理解することです。.then()の挙動、特に新しいPromiseを返す仕組みやエラー伝播のルールを理解していれば、async/awaitで予期しない動作に遭遇した場合でも、その原因を突き止めやすくなります。

まとめ

この記事では、JavaScriptの.then()メソッドについて、その基本的な使い方からPromiseチェーン、エラーハンドリング、そしてasync/awaitとの関連まで、詳しく解説しました。

  • 非同期処理は現代JavaScriptにおいて不可欠であり、従来のコールバック関数はコールバック地獄という問題を引き起こしました。
  • Promiseは非同期操作の最終的な完了や失敗とその結果を表すオブジェクトであり、非同期処理の状態管理を改善しました。Promiseにはpending, fulfilled, rejectedの3つの状態があります。
  • .then()メソッドは、Promiseが解決(fulfilledまたはrejected)されたときに実行されるコールバック関数を登録するためのメソッドです。構文はpromise.then(onFulfilled, onRejected)です。
  • onFulfilledは成功時に、onRejectedは失敗時に実行され、それぞれ成功値または拒否理由を受け取ります。
  • .then() メソッドは呼び出されるたびに必ず新しいPromiseオブジェクトを返します。この性質を利用して、複数の非同期処理を順番に実行するPromiseチェーンを構築できます。
  • Promiseチェーンの途中で発生したエラーはチェーンを伝播し、次に現れるエラーハンドラー(.then()の第2引数か.catch())によって捕捉されます。
  • .catch()メソッドは、.then(undefined, onRejected)の糖衣構文であり、エラーハンドリングに特化したメソッドです。.catch()をチェーンの最後に配置することで、チェーン全体のエラーをまとめて捕捉できるため、推奨されるエラーハンドリングパターンです。
  • async/await構文はPromiseと.then()の糖衣構文であり、非同期コードをより同期的に記述できるようにしますが、その基盤にはPromiseのメカニズムがあります。async関数はPromiseを返し、そのPromiseに対して.then().catch()を使用できます。

.then()メソッドは、Promiseベースの非同期処理APIを使用する上で最も基本的な、そして強力なツールです。その動作原理、特に新しいPromiseを返す仕組みとチェーンの構築方法、そしてエラー伝播のルールをしっかりと理解することは、JavaScriptで効率的かつ堅牢な非同期コードを書くために不可欠です。

Promiseと.then()は、非同期処理の世界を大きく変え、今日のモダンなJavaScript開発の基盤となっています。この記事を通じて、.then()メソッドへの理解が深まり、より自信を持って非同期処理を扱えるようになることを願っています。


コメントする

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

上部へスクロール