JavaScript Promiseの使い方完全ガイド | 基礎から実践まで徹底解説

JavaScriptのPromiseについて、基本的な使い方から実践的な活用方法まで網羅的に学べます。非同期処理における「コールバック地獄」の解決方法、then/catch/finallyを使った処理の連鎖、Promise.all()やPromise.any()などの並行処理、async/awaitとの組み合わせ、さらにエラーハンドリングの実装方法まで、具体的なコード例とともに理解できます。初心者から実務で使いたい方まで対応した内容です。

“`html

目次

JavaScriptのPromiseとは

javascript+promise+async

JavaScriptのPromiseは、非同期処理をより直感的かつ効率的に扱うための仕組みです。従来のコールバック関数による非同期処理の記述方法では、複雑な処理を実装する際にコードの可読性が著しく低下する問題がありました。Promiseはこの課題を解決し、非同期処理の結果を「約束(Promise)」として表現することで、より明確で保守性の高いコードを書くことを可能にします。

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

JavaScriptにおける処理の実行方法には、同期処理と非同期処理の2つがあります。この違いを理解することは、Promiseを効果的に活用する上で非常に重要です。

同期処理は、コードが記述された順番に一つずつ実行され、前の処理が完了するまで次の処理は開始されません。例えば、計算処理やローカル変数への代入などは同期的に実行されます。

console.log('処理1');
console.log('処理2');
console.log('処理3');
// 出力: 処理1 → 処理2 → 処理3(順番通り)

一方、非同期処理は、処理の完了を待たずに次の処理を実行できる仕組みです。サーバーへのデータ取得、ファイルの読み込み、タイマー処理などが代表的な非同期処理です。非同期処理を使用すると、時間のかかる処理を実行している間も、プログラム全体が停止することなく他の処理を継続できます。

console.log('処理1');
setTimeout(() => {
  console.log('処理2(非同期)');
}, 1000);
console.log('処理3');
// 出力: 処理1 → 処理3 → 処理2(非同期)

このように、非同期処理では実行順序がコードの記述順とは異なる場合があります。特にWebアプリケーション開発において、ユーザー体験を損なわないためには非同期処理の適切な活用が不可欠です。

コールバック関数の課題

Promiseが登場する以前、JavaScriptで非同期処理を扱う主要な方法はコールバック関数でした。コールバック関数とは、非同期処理の完了時に実行される関数を引数として渡す手法です。

getData(function(result1) {
  processData(result1, function(result2) {
    saveData(result2, function(result3) {
      console.log('完了');
    });
  });
});

しかし、コールバック関数には深刻な問題がありました。最も顕著なのが「コールバック地獄(Callback Hell)」と呼ばれる現象です。複数の非同期処理を順次実行する必要がある場合、コールバック関数をネストさせることになり、コードが階段状に深くなっていきます。

コールバック地獄による主な問題点は以下の通りです。

  • 可読性の低下: ネストが深くなるほどコードの流れを追うことが困難になる
  • 保守性の悪化: コードの修正や機能追加が非常に難しくなる
  • エラーハンドリングの複雑化: 各コールバックで個別にエラー処理を記述する必要がある
  • デバッグの困難さ: エラーが発生した際の追跡が難しい

さらに、複数の非同期処理を並列実行したい場合や、特定の条件で処理をスキップしたい場合など、少し複雑な制御フローを実装しようとすると、コールバック関数だけでは非常に煩雑なコードになってしまいます。

Promiseが解決する問題

JavaScriptのPromiseは、コールバック関数が抱える様々な課題を解決するために設計された仕組みです。Promiseを使用することで、非同期処理をよりシンプルかつ直感的に記述できるようになります。

Promiseがもたらす主なメリットは以下の通りです。

  • コードの平坦化: メソッドチェーンを使った記述により、ネストの深さを大幅に削減できる
  • 統一的なエラーハンドリング: catch()メソッドで一括してエラーを処理できる
  • 処理の状態管理: 非同期処理の状態(pending/fulfilled/rejected)を明確に管理できる
  • 合成可能性の向上: 複数のPromiseを組み合わせた複雑な処理フローを簡潔に表現できる

例えば、先ほどのコールバック地獄のコードをPromiseで書き換えると、以下のように平坦で読みやすいコードになります。

getData()
  .then(result1 => processData(result1))
  .then(result2 => saveData(result2))
  .then(result3 => console.log('完了'))
  .catch(error => console.error('エラー:', error));

このようにPromiseを使用することで、非同期処理の流れが上から下へと一直線に記述でき、コードの意図が明確になります。また、エラー処理も一箇所のcatch()でまとめて記述できるため、保守性が大幅に向上します。

さらにPromiseは、ES2015(ES6)で正式にJavaScriptの標準仕様として採用され、モダンなJavaScript開発において欠かせない基礎技術となっています。fetchAPIやasync/awaitといった最新の非同期処理機能も、すべてPromiseを基盤として構築されています。

“`

“`html

Promiseの基本的な使い方

javascript+promise+coding

JavaScriptのPromiseを活用するには、その基本的な構文と仕組みを理解することが不可欠です。このセクションでは、Promiseインスタンスの生成から基本的な書き方、そしてPromiseが持つ3つの状態について詳しく解説していきます。これらの基礎知識を身につけることで、非同期処理を効果的にコントロールできるようになります。

Promiseインスタンスの生成方法

Promiseインスタンスはnew Promise()コンストラクタを使用して生成します。コンストラクタには実行関数(executor)を引数として渡し、この関数はresolverejectという2つのパラメータを受け取ります。

const myPromise = new Promise((resolve, reject) => {
  // 非同期処理をここに記述
  const success = true;
  
  if (success) {
    resolve('処理が成功しました');
  } else {
    reject('処理が失敗しました');
  }
});

この実行関数は、Promiseインスタンスが生成されると同時に即座に実行されます。resolve関数を呼び出すと処理が成功したことを示し、reject関数を呼び出すと処理が失敗したことを示します。それぞれの関数には、後続の処理に渡したい値や情報を引数として設定できます。

実践的な例として、タイマー処理を含むPromiseを見てみましょう。

const delayPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('3秒経過しました');
  }, 3000);
});

このコードでは、3秒後にPromiseが解決され、指定したメッセージが返されます。このように、Promiseは非同期処理を扱うための明確な構造を提供します。

基本的な構文と書き方

Promiseの基本的な構文は、生成したPromiseインスタンスに対してthen()catch()finally()といったメソッドをチェーンして記述します。これにより、非同期処理の成功時・失敗時・完了時の処理を順序立てて記述できます。

const promise = new Promise((resolve, reject) => {
  const randomNum = Math.random();
  
  if (randomNum > 0.5) {
    resolve(randomNum);
  } else {
    reject(new Error('値が0.5以下です'));
  }
});

promise
  .then((value) => {
    console.log('成功:', value);
  })
  .catch((error) => {
    console.error('失敗:', error.message);
  })
  .finally(() => {
    console.log('処理完了');
  });

このようにメソッドチェーンで記述することで、非同期処理の流れを直感的に表現できます。then()メソッドには成功時のコールバック関数を、catch()メソッドには失敗時のコールバック関数を渡します。

また、Promiseは以下のような簡潔な書き方も可能です。

// アロー関数を使った簡潔な記述
new Promise((resolve, reject) => {
  // 処理内容
  resolve('データ取得完了');
})
.then(result => console.log(result))
.catch(error => console.error(error));

この書き方により、コードの可読性が向上し、非同期処理の意図が明確になります。

Promiseの3つの状態について

Promiseインスタンスは常に3つの状態のいずれかを保持しています。この状態管理がPromiseの核心的な機能であり、非同期処理の進行状況を正確に把握できる仕組みとなっています。

状態 英語表記 説明
待機 Pending 初期状態。非同期処理がまだ完了していない状態
成功 Fulfilled 処理が正常に完了した状態。resolveが呼ばれた状態
失敗 Rejected 処理が失敗した状態。rejectが呼ばれた状態

Promiseは生成時にはPending(待機)状態からスタートします。その後、非同期処理の結果に応じて、Fulfilled(成功)またはRejected(失敗)のいずれかの状態に遷移します。

const checkPromiseState = new Promise((resolve, reject) => {
  console.log('状態: Pending');
  
  setTimeout(() => {
    resolve('完了');
    console.log('状態: Fulfilled');
  }, 2000);
});

checkPromiseState.then(result => {
  console.log('結果:', result);
});

重要な点として、Promiseは一度Fulfilled(成功)またはRejected(失敗)の状態になると、その状態は変更されません。これを「settled(確定)」状態と呼びます。つまり、resolve()やreject()を複数回呼び出しても、最初の呼び出しのみが有効となり、その後の呼び出しは無視されます。

const immutablePromise = new Promise((resolve, reject) => {
  resolve('最初の解決');
  resolve('2回目の解決'); // これは無視される
  reject('これも無視される'); // これも無視される
});

immutablePromise.then(value => {
  console.log(value); // '最初の解決' のみが出力される
});

この不変性により、Promiseは予測可能で安全な非同期処理の制御を実現しています。状態遷移は一方向のみで、Pending → Fulfilled または Pending → Rejected という流れを持ち、逆方向への遷移や複数回の状態変更は発生しません。

  • Pending状態では、まだ結果が確定していないため、then()やcatch()のコールバックは実行されません
  • Fulfilled状態になると、then()メソッドの最初の引数として渡したコールバック関数が実行されます
  • Rejected状態になると、catch()メソッドまたはthen()の2番目の引数として渡したエラーハンドラが実行されます
  • settled(確定)状態になったPromiseは、その結果を保持し続け、後から登録されたコールバックにも同じ結果が渡されます

これらの状態管理の仕組みを理解することで、javascript promiseを使った非同期処理をより確実に制御できるようになります。

“`

“`html

Promiseの主要メソッド

javascript+promise+async

JavaScriptのPromiseには、非同期処理の結果に応じて実行する処理を制御するための主要なメソッドが用意されています。これらのメソッドを適切に組み合わせることで、非同期処理の成功・失敗・完了といった様々な状況に対応できます。ここでは、Promiseオブジェクトで最も頻繁に使用される3つのメソッドについて詳しく解説していきます。

then()による成功時の処理

then()メソッドは、Promiseが成功(fulfilled)状態になった際に実行される処理を定義するための最も基本的なメソッドです。このメソッドは最大2つのコールバック関数を引数として受け取ることができ、第1引数には成功時の処理、第2引数には失敗時の処理を指定します。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('処理が成功しました');
  }, 1000);
});

promise.then((result) => {
  console.log(result); // '処理が成功しました'
});

then()メソッドの重要な特徴は、新しいPromiseオブジェクトを返すという点です。これにより、複数のthen()を連鎖させるPromiseチェーンの実装が可能になります。コールバック関数の戻り値は、次のthen()に渡されるため、段階的なデータ変換や処理の連結が直感的に記述できます。

promise
  .then((result) => {
    return result + 'さらに処理を追加';
  })
  .then((newResult) => {
    console.log(newResult); // '処理が成功しましたさらに処理を追加'
  });

また、then()内で新たなPromiseを返すこともでき、非同期処理を順次実行する際に非常に便利です。この仕組みにより、コールバック地獄を回避した読みやすいコードが実現できます。

catch()による失敗時の処理

catch()メソッドは、Promiseが失敗(rejected)状態になった際のエラー処理を定義するためのメソッドです。Promiseチェーン内で発生したエラーや明示的なreject()呼び出しを捕捉し、適切なエラーハンドリングを実行できます。

const errorPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('エラーが発生しました'));
  }, 1000);
});

errorPromise.catch((error) => {
  console.error('エラー内容:', error.message); // 'エラーが発生しました'
});

catch()は、実質的にthen(null, onRejected)と同じ動作をしますが、エラー処理を明示的に記述できるため可読性が向上します。特に、Promiseチェーンの末尾にcatch()を配置することで、チェーン内のどの段階で発生したエラーもまとめて捕捉できる利点があります。

promise
  .then((result) => {
    throw new Error('処理中にエラー発生');
  })
  .then((data) => {
    // この処理は実行されない
    console.log(data);
  })
  .catch((error) => {
    console.error('捕捉されたエラー:', error.message);
  });

catch()メソッドも新しいPromiseを返すため、エラーを処理した後に再度then()を繋げることができます。これにより、エラーからの回復処理や代替処理の実装が可能になります。

finally()による常時実行処理

finally()メソッドは、Promiseの結果が成功・失敗のどちらであっても必ず実行される処理を定義するためのメソッドです。このメソッドは、ES2018(ES9)で導入され、リソースのクリーンアップやローディング表示の終了など、結果に関わらず必ず実行すべき処理に適しています。

let isLoading = true;

fetch('https://api.example.com/data')
  .then((response) => {
    console.log('データ取得成功');
    return response.json();
  })
  .catch((error) => {
    console.error('データ取得失敗');
  })
  .finally(() => {
    isLoading = false;
    console.log('通信処理が完了しました');
  });

finally()の特徴として、コールバック関数には引数が渡されないという点があります。これは、成功・失敗のどちらでも実行されるため、結果の値やエラーオブジェクトを受け取る必要がないためです。また、finally()内で値を返したり例外をスローしない限り、元のPromiseの結果がそのまま次に伝播します。

Promise.resolve('元の値')
  .finally(() => {
    console.log('finally実行');
    return '新しい値'; // この値は無視される
  })
  .then((value) => {
    console.log(value); // '元の値'が出力される
  });

実際の開発現場では、then()catch()finally()を組み合わせることで、堅牢で保守性の高い非同期処理を実装できます。特にAPI通信やファイル操作など、成功・失敗の両方のケースを考慮する必要がある処理において、これらのメソッドは不可欠な存在となっています。

メソッド 実行タイミング 引数 主な用途
then() 成功時(fulfilled) 成功時の値 非同期処理の結果を受け取り、次の処理へ繋げる
catch() 失敗時(rejected) エラーオブジェクト エラーハンドリングと例外処理
finally() 常時(成功・失敗問わず) なし クリーンアップ処理やリソース解放

“`

Promiseチェーンの実装

javascript+promise+coding

JavaScriptのPromiseでは、非同期処理を順次実行するために「Promiseチェーン」という手法が広く用いられます。Promiseチェーンを活用することで、複数の非同期処理を可読性高く、かつメンテナンスしやすい形で記述することができます。このセクションでは、Promiseチェーンの基本的な実装方法から、実践的な活用テクニックまでを詳しく解説していきます。

Promiseチェーンの基本的な書き方

Promiseチェーンは、then()メソッドを連鎖的に呼び出すことで実現します。then()メソッドは新しいPromiseオブジェクトを返すため、その返り値に対してさらにthen()を繋げることができます。この仕組みにより、複数の非同期処理を順序立てて実行することが可能になります。

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

someAsyncFunction()
  .then((result1) => {
    console.log(result1);
    return nextAsyncFunction(result1);
  })
  .then((result2) => {
    console.log(result2);
    return anotherAsyncFunction(result2);
  })
  .then((result3) => {
    console.log(result3);
  })
  .catch((error) => {
    console.error('エラーが発生しました:', error);
  });

この例では、最初の非同期関数の結果を受け取り、それを次の処理に渡すという流れを繰り返しています。then()メソッドは前の処理が完了するまで実行されないため、確実に順序を保証できます。

実践的な例として、ユーザー情報を取得してから関連データを順次取得する処理を見てみましょう。

fetchUser(userId)
  .then((user) => {
    console.log('ユーザー情報を取得:', user.name);
    return fetchUserPosts(user.id);
  })
  .then((posts) => {
    console.log('投稿数:', posts.length);
    return fetchPostComments(posts[0].id);
  })
  .then((comments) => {
    console.log('コメント数:', comments.length);
  })
  .catch((error) => {
    console.error('処理に失敗しました:', error);
  });

このようにPromiseチェーンを使うことで、ネストが深くなる「コールバック地獄」を回避し、処理の流れが明確なコードを記述できます。

チェーン内で値を返す方法

Promiseチェーンを効果的に活用するには、then()内で適切に値を返す方法を理解する必要があります。then()のコールバック関数内で返す値によって、次のthen()に渡される値が変わります。

値を返す方法には主に3つのパターンがあります。

  • 通常の値を返す – 同期的な値を返すと、その値で解決されたPromiseが次のthen()に渡されます
  • Promiseを返す – Promiseオブジェクトを返すと、そのPromiseが解決されるまで待機し、解決された値が次に渡されます
  • 何も返さないundefinedが次のthen()に渡されます

具体的なコード例で見ていきましょう。

// 通常の値を返す例
Promise.resolve(5)
  .then((num) => {
    return num * 2; // 10が次のthen()に渡される
  })
  .then((result) => {
    console.log(result); // 10
    return result + 5; // 15が次のthen()に渡される
  })
  .then((finalResult) => {
    console.log(finalResult); // 15
  });

// Promiseを返す例
Promise.resolve('ユーザーID')
  .then((userId) => {
    // 新しいPromiseを返す
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(`${userId}のデータ`);
      }, 1000);
    });
  })
  .then((userData) => {
    console.log(userData); // 'ユーザーIDのデータ'(1秒後)
  });

注意点として、then()内で値を返し忘れると、次のthen()にはundefinedが渡されてしまいます。これは意図しないバグの原因となるため、必要な値は明示的にreturn文で返すようにしましょう。

// 悪い例:returnを忘れている
fetchData()
  .then((data) => {
    processData(data); // returnを書いていない
  })
  .then((result) => {
    console.log(result); // undefined
  });

// 良い例:適切にreturnしている
fetchData()
  .then((data) => {
    return processData(data);
  })
  .then((result) => {
    console.log(result); // 正しい値が表示される
  });

複数のPromise処理を連結する手法

実際の開発では、複数の異なる非同期処理を組み合わせることが頻繁にあります。Promiseチェーンを使って複数の処理を連結する際には、いくつかの実践的なパターンがあります。

まず、単純な連結パターンでは、各処理の結果を次の処理に順次渡していきます。

function validateInput(input) {
  return new Promise((resolve, reject) => {
    if (input) {
      resolve(input);
    } else {
      reject(new Error('入力が空です'));
    }
  });
}

function sanitizeData(data) {
  return new Promise((resolve) => {
    const sanitized = data.trim().toLowerCase();
    resolve(sanitized);
  });
}

function saveToDatabase(data) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 123, data: data });
    }, 500);
  });
}

// 複数の処理を連結
validateInput('  Sample Data  ')
  .then(sanitizeData)
  .then(saveToDatabase)
  .then((result) => {
    console.log('保存完了:', result);
  })
  .catch((error) => {
    console.error('処理失敗:', error.message);
  });

より複雑なシナリオでは、途中の処理結果を後続の処理でも使いたい場合があります。このような場合は、変数のスコープを活用するか、オブジェクトに結果を格納していく手法が有効です。

// スコープを活用するパターン
let userData;

fetchUser(userId)
  .then((user) => {
    userData = user; // 外部変数に保存
    return fetchUserSettings(user.id);
  })
  .then((settings) => {
    // userDataとsettingsの両方を使える
    return applySettings(userData, settings);
  })
  .then((result) => {
    console.log('設定適用完了:', result);
  });

// オブジェクトに格納するパターン
fetchUser(userId)
  .then((user) => {
    return fetchUserSettings(user.id).then((settings) => {
      return { user, settings }; // オブジェクトにまとめる
    });
  })
  .then(({ user, settings }) => {
    // 分割代入で取り出す
    return applySettings(user, settings);
  })
  .then((result) => {
    console.log('設定適用完了:', result);
  });

後者のオブジェクトに格納する手法は、関数型プログラミングの原則に則っており、外部変数への依存がないため、より保守性の高いコードになります。

逐次処理の実装方法

配列で管理された複数のタスクを順番に実行したい場合、逐次処理(シーケンシャル処理)の実装が必要になります。これは並列処理とは異なり、前のタスクが完了してから次のタスクを開始する処理方式です。

最も基本的な逐次処理の実装方法は、reduce()メソッドを使ってPromiseチェーンを動的に構築する手法です。

const tasks = [task1, task2, task3, task4];

tasks.reduce((promiseChain, currentTask) => {
  return promiseChain.then((results) => {
    return currentTask().then((result) => {
      return [...results, result];
    });
  });
}, Promise.resolve([]))
  .then((allResults) => {
    console.log('全ての処理が完了:', allResults);
  });

この手法では、reduce()が各タスクを順次チェーンに追加していき、前のタスクの結果を蓄積しながら処理を進めます。

より実践的な例として、複数のAPIエンドポイントに順番にリクエストを送る処理を見てみましょう。

const urls = [
  'https://api.example.com/data1',
  'https://api.example.com/data2',
  'https://api.example.com/data3'
];

function fetchSequentially(urls) {
  return urls.reduce((chain, url) => {
    return chain.then((results) => {
      console.log(`取得中: ${url}`);
      return fetch(url)
        .then(response => response.json())
        .then(data => {
          console.log(`完了: ${url}`);
          return [...results, data];
        });
    });
  }, Promise.resolve([]));
}

fetchSequentially(urls)
  .then((allData) => {
    console.log('全データ取得完了:', allData);
  })
  .catch((error) => {
    console.error('エラー発生:', error);
  });

別のアプローチとして、for...ofループとawaitを組み合わせる方法もあります(async/awaitの詳細は別セクションで扱いますが、参考として紹介します)。

async function processSequentially(items) {
  const results = [];
  
  for (const item of items) {
    const result = await processItem(item);
    results.push(result);
    console.log(`処理完了: ${item}`);
  }
  
  return results;
}

逐次処理は並列処理と比べて処理時間が長くなりますが、APIのレート制限を守る必要がある場合や、前の処理結果に依存する場合など、順序が重要なケースでは必須の実装パターンです。

また、エラーハンドリングを含めたより堅牢な逐次処理の実装例も紹介します。

function sequentialProcess(tasks) {
  return tasks.reduce((chain, task, index) => {
    return chain
      .then((results) => {
        return task()
          .then((result) => {
            console.log(`タスク${index + 1}完了`);
            return [...results, { success: true, data: result }];
          })
          .catch((error) => {
            console.error(`タスク${index + 1}失敗:`, error);
            return [...results, { success: false, error: error.message }];
          });
      });
  }, Promise.resolve([]))
  .then((allResults) => {
    const successCount = allResults.filter(r => r.success).length;
    console.log(`成功: ${successCount}/${allResults.length}`);
    return allResults;
  });
}

// 使用例
const taskList = [
  () => fetchData('endpoint1'),
  () => fetchData('endpoint2'),
  () => fetchData('endpoint3')
];

sequentialProcess(taskList)
  .then((results) => {
    console.log('全処理結果:', results);
  });

逐次処理を実装する際の注意点として、処理数が多い場合はユーザー体験を考慮し、プログレス表示やキャンセル機能の実装も検討すべきです。長時間かかる処理では、ユーザーに進捗状況をフィードバックすることが重要になります。

“`html

Promiseの静的メソッド

javascript+promise+async

JavaScriptのPromiseには、複数の非同期処理を効率的に扱うための静的メソッドが用意されています。これらのメソッドを活用することで、複雑な非同期処理のパターンを簡潔に記述できるようになります。Promise.all()やPromise.race()などの静的メソッドは、複数のPromiseを同時に管理する際に非常に便利で、実務でも頻繁に使用される重要な機能です。

Promise.all()で全てのタスクを待機

Promise.all()は、複数のPromiseを並列実行し、全てが成功するまで待機するメソッドです。配列で渡された全てのPromiseがresolveされた時点で、それぞれの結果を配列として返します。

const promise1 = Promise.resolve(10);
const promise2 = Promise.resolve(20);
const promise3 = Promise.resolve(30);

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log(results); // [10, 20, 30]
  })
  .catch((error) => {
    console.error('いずれかのPromiseが失敗しました', error);
  });

Promise.all()の特徴として、いずれか1つでもrejectされると即座に全体が失敗となります。そのため、全てのタスクが成功することが前提の処理に適しています。例えば、複数のAPIから必要なデータを取得する場合や、複数の画像ファイルを一括で読み込む処理などに利用されます。

Promise.race()でいずれかのタスク完了を検知

Promise.race()は、複数のPromiseの中で最初に完了したもの(resolveまたはreject)の結果を返すメソッドです。その名の通り、Promiseの「レース」を行い、最も早く終わったものが採用されます。

const slow = new Promise((resolve) => {
  setTimeout(() => resolve('遅い処理'), 3000);
});

const fast = new Promise((resolve) => {
  setTimeout(() => resolve('速い処理'), 1000);
});

Promise.race([slow, fast])
  .then((result) => {
    console.log(result); // '速い処理'(1秒後に実行)
  });

Promise.race()は、タイムアウト処理の実装に特に有効です。実際の処理とタイムアウト用のPromiseを競わせることで、一定時間内に処理が完了しない場合にエラーハンドリングを行うことができます。

Promise.any()で最初の成功を取得

Promise.any()は、複数のPromiseの中で最初にresolveされたものの結果を返すメソッドです。Promise.race()と似ていますが、Promise.any()はrejectされたPromiseを無視し、最初の成功を待ち続ける点が異なります。

const promise1 = Promise.reject('エラー1');
const promise2 = new Promise((resolve) => {
  setTimeout(() => resolve('成功2'), 1000);
});
const promise3 = new Promise((resolve) => {
  setTimeout(() => resolve('成功3'), 2000);
});

Promise.any([promise1, promise2, promise3])
  .then((result) => {
    console.log(result); // '成功2'
  })
  .catch((error) => {
    console.error('全てのPromiseが失敗しました', error);
  });

Promise.any()は、全てのPromiseがrejectされた場合にのみエラーとなります。複数のサーバーから同じリソースを取得する際、いずれか1つでも成功すれば良い場合などに適しています。

Promise.allSettled()で全タスクの結果を取得

Promise.allSettled()は、全てのPromiseが完了するまで待機し、成功・失敗に関わらず全ての結果を返すメソッドです。Promise.all()とは異なり、一部が失敗しても処理は中断されません。

const promise1 = Promise.resolve('成功1');
const promise2 = Promise.reject('エラー2');
const promise3 = Promise.resolve('成功3');

Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    console.log(results);
    // [
    //   { status: 'fulfilled', value: '成功1' },
    //   { status: 'rejected', reason: 'エラー2' },
    //   { status: 'fulfilled', value: '成功3' }
    // ]
  });

各結果はオブジェクトとして返され、statusプロパティで’fulfilled’(成功)か’rejected’(失敗)かを判別できます。成功した場合はvalueプロパティに結果が、失敗した場合はreasonプロパティにエラー情報が格納されます。複数の独立したタスクを実行し、個別の成否を確認したい場合に非常に有用です。

Promise.resolve()とPromise.reject()の活用

Promise.resolve()とPromise.reject()は、即座にresolveまたはrejectされたPromiseを生成する静的メソッドです。既に値が確定している場合や、テストコードの記述、Promiseチェーンの開始点として利用されます。

// Promise.resolve()の使用例
Promise.resolve(42)
  .then((value) => {
    console.log(value); // 42
  });

// Promise.reject()の使用例
Promise.reject(new Error('エラーが発生しました'))
  .catch((error) => {
    console.error(error.message);
  });

これらのメソッドは、同期的な値をPromiseとして扱いたい場合や、条件によってPromiseを返すかどうかを制御する関数を実装する際に便利です。また、非Promise値をPromise化することで、統一的なインターフェースでの処理が可能になります。

function getValueAsPromise(value) {
  if (value > 0) {
    return Promise.resolve(value);
  } else {
    return Promise.reject(new Error('値は正の数である必要があります'));
  }
}

Promise.withResolvers()によるプロミス生成

Promise.withResolvers()は、Promiseとそのresolveおよびrejectメソッドを別々に取得できる静的メソッドです。この機能により、Promiseの生成とその解決を分離して扱うことが可能になります。

const { promise, resolve, reject } = Promise.withResolvers();

// 別の場所でPromiseを解決できる
setTimeout(() => {
  resolve('処理が完了しました');
}, 2000);

// Promiseは通常通り使用できる
promise.then((result) => {
  console.log(result); // '処理が完了しました'
});

従来のPromiseコンストラクタでは、resolveとrejectを外部から参照するために変数へ代入する必要がありましたが、Promise.withResolvers()を使うことで、よりシンプルに記述できます。イベントハンドラーや複雑な非同期フローの制御において、Promiseの解決タイミングを柔軟に管理したい場合に特に有効です。

ただし、Promise.withResolvers()は比較的新しい機能であるため、ブラウザやNode.jsのバージョンによっては対応していない場合があります。使用する際には、実行環境の互換性を確認することをおすすめします。

“`

“`html

Async/Awaitの使い方

javascript+async+programming

Async/AwaitはECMAScript 2017(ES8)で導入された構文で、Promiseをより直感的に扱えるようにする記法です。非同期処理をあたかも同期処理のように記述できるため、コードの可読性が大幅に向上し、複雑な非同期処理も理解しやすくなります。ここでは、Async/Awaitの基本的な使い方から実践的なテクニックまでを解説します。

Async Functionの定義方法

Async Functionは、関数の前にasyncキーワードを付けることで定義します。async宣言された関数は、自動的にPromiseを返す関数になります。関数内で明示的にPromiseを返さなくても、戻り値は自動的にPromiseでラップされます。

// 基本的なAsync Functionの定義
async function fetchData() {
  return 'データ取得完了';
}

// 実行結果はPromiseとして返される
fetchData().then(result => {
  console.log(result); // 'データ取得完了'
});

// アロー関数での定義
const getData = async () => {
  return 'データ';
};

// メソッドとしての定義
const obj = {
  async fetchUser() {
    return { name: 'Taro', age: 25 };
  }
};

Async Functionの特徴として、関数内でreturnした値は自動的にPromise.resolve()でラップされ、エラーがスローされた場合はPromise.reject()でラップされます。このため、呼び出し側では通常のPromiseと同じようにthen()catch()を使って処理できます。

await式の基本構文

await式は、Promiseの解決を待つために使用します。awaitはasync関数内でのみ使用可能で、Promiseが解決されるまで関数の実行を一時停止します。これにより、非同期処理を同期処理のような直感的な記述で扱えます。

async function getUserData() {
  // Promiseが解決されるまで待機
  const response = await fetch('https://api.example.com/user');
  const data = await response.json();
  
  console.log(data);
  return data;
}

// 複数のawaitを使った例
async function processData() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts[0].id);
  
  return { user, posts, comments };
}

await式を使うことで、コールバック地獄やPromiseチェーンの複雑さを解消できます。ただし、await式はasync関数の外では使用できないため、トップレベルで使用する場合はasync関数でラップする必要があります(モジュールのトップレベルawaitを除く)。

// エラーハンドリングを含む例
async function fetchWithErrorHandling() {
  try {
    const result = await fetch('https://api.example.com/data');
    const data = await result.json();
    return data;
  } catch (error) {
    console.error('データ取得エラー:', error);
    throw error;
  }
}

PromiseチェーンをAsync/Awaitで書き換える

既存のPromiseチェーンは、Async/Awaitを使って書き換えることで、より読みやすく保守性の高いコードに改善できます。複雑にネストしたPromiseチェーンも、await式を使うことで順序立てた処理として表現できます。

Promiseチェーンの例:

function getUserPosts() {
  return fetchUser()
    .then(user => {
      console.log('ユーザー取得:', user.name);
      return fetchPosts(user.id);
    })
    .then(posts => {
      console.log('投稿数:', posts.length);
      return fetchComments(posts[0].id);
    })
    .then(comments => {
      console.log('コメント数:', comments.length);
      return comments;
    })
    .catch(error => {
      console.error('エラー:', error);
      throw error;
    });
}

Async/Awaitで書き換えた例:

async function getUserPosts() {
  try {
    const user = await fetchUser();
    console.log('ユーザー取得:', user.name);
    
    const posts = await fetchPosts(user.id);
    console.log('投稿数:', posts.length);
    
    const comments = await fetchComments(posts[0].id);
    console.log('コメント数:', comments.length);
    
    return comments;
  } catch (error) {
    console.error('エラー:', error);
    throw error;
  }
}

このように書き換えることで、処理の流れが上から下へと自然に読めるようになります。エラーハンドリングもtry...catch構文で統一的に扱えるため、コードの意図が明確になります。

Async Functionと反復処理の組み合わせ

Async/Awaitは、ループ処理と組み合わせることで、複数の非同期処理を順次実行したり並列実行したりできます。適切な実装方法を選択することで、パフォーマンスと可読性を両立できます。

順次実行(for…ofループ):

async function processUsersSequentially(userIds) {
  const results = [];
  
  for (const id of userIds) {
    const user = await fetchUser(id);
    const processed = await processUser(user);
    results.push(processed);
  }
  
  return results;
}

// 各処理が順番に実行される(前の処理が完了してから次へ)

並列実行(map + Promise.all):

async function processUsersInParallel(userIds) {
  const promises = userIds.map(async (id) => {
    const user = await fetchUser(id);
    return await processUser(user);
  });
  
  const results = await Promise.all(promises);
  return results;
}

// 全ての処理が同時に開始され、全て完了するまで待機

注意点として、通常のforEachループ内ではawaitが期待通りに動作しません。forEachのコールバック関数がasyncであっても、forEach自体は各Promiseの完了を待たないため、for…ofやmapを使用する必要があります。

// 誤った実装例
async function wrongImplementation(userIds) {
  const results = [];
  
  // これは期待通りに動作しない
  userIds.forEach(async (id) => {
    const user = await fetchUser(id);
    results.push(user);
  });
  
  return results; // 空の配列が返される可能性が高い
}

// 正しい実装例
async function correctImplementation(userIds) {
  const results = [];
  
  for (const id of userIds) {
    const user = await fetchUser(id);
    results.push(user);
  }
  
  return results;
}

Async FunctionとPromise APIの併用

Async/AwaitとPromiseの静的メソッドを組み合わせることで、より高度な非同期処理制御が可能になります。複数の非同期処理を効率的に管理し、アプリケーションのパフォーマンスを最適化できます。

Promise.allとの併用:

async function fetchMultipleResources() {
  try {
    // 複数の非同期処理を並列実行
    const [users, posts, comments] = await Promise.all([
      fetchUsers(),
      fetchPosts(),
      fetchComments()
    ]);
    
    return { users, posts, comments };
  } catch (error) {
    console.error('いずれかの取得に失敗:', error);
    throw error;
  }
}

Promise.raceとの併用:

async function fetchWithTimeout(url, timeout = 5000) {
  const fetchPromise = fetch(url);
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('タイムアウト')), timeout);
  });
  
  try {
    const response = await Promise.race([fetchPromise, timeoutPromise]);
    return await response.json();
  } catch (error) {
    console.error('取得失敗:', error.message);
    throw error;
  }
}

Promise.allSettledとの併用:

async function fetchAllUsersWithResults(userIds) {
  const promises = userIds.map(id => fetchUser(id));
  const results = await Promise.allSettled(promises);
  
  const succeeded = results
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);
  
  const failed = results
    .filter(r => r.status === 'rejected')
    .map(r => r.reason);
  
  console.log(`成功: ${succeeded.length}件, 失敗: ${failed.length}件`);
  
  return { succeeded, failed };
}

このように、Async/AwaitとPromise APIを組み合わせることで、柔軟で強力な非同期処理が実現できます。処理の性質に応じて適切なAPIを選択することで、コードの表現力とパフォーマンスを両立できます。

メソッド 使用場面 特徴
Promise.all 全ての処理が成功する必要がある場合 1つでも失敗すると全体が失敗
Promise.race 最初の結果だけが必要な場合 タイムアウト処理に有用
Promise.allSettled 全ての結果を取得したい場合 成功・失敗に関わらず全結果を取得
Promise.any 1つでも成功すれば良い場合 最初の成功結果を返す

“`

“`html

Promiseのエラーハンドリング

javascript+promise+error

JavaScriptのPromiseを使った非同期処理において、エラーハンドリングは極めて重要です。適切なエラー処理を実装しないと、予期しない動作やアプリケーションのクラッシュを引き起こす可能性があります。Promiseには独自のエラー処理の仕組みが備わっており、同期処理とは異なる方法でエラーを捕捉・処理します。このセクションでは、Promiseにおけるエラーハンドリングの基本から実践的な実装方法まで詳しく解説します。

Promise内での例外処理の仕組み

Promiseの内部で発生したエラーは、自動的にPromiseのrejected状態に変換されます。これは通常のtry-catch文とは異なる、Promiseならではの特徴です。

Promise内で例外が発生すると、以下のような流れでエラーが処理されます:

const promise = new Promise((resolve, reject) => {
  throw new Error('エラーが発生しました');
  // この例外は自動的にrejectに変換される
});

promise.catch(error => {
  console.error('エラーを捕捉:', error.message);
  // 出力: エラーを捕捉: エラーが発生しました
});

Promiseのexecutor関数内(new Promise内)でthrowされたエラーは、明示的にreject()を呼び出したのと同じ効果を持ちます。これにより、同期的なエラーも非同期処理のエラーハンドリングフローに統合できます。

// throwとrejectは実質的に同じ結果になる
const promise1 = new Promise((resolve, reject) => {
  throw new Error('throwによるエラー');
});

const promise2 = new Promise((resolve, reject) => {
  reject(new Error('rejectによるエラー'));
});

// どちらも同じようにcatchで捕捉できる
promise1.catch(err => console.error(err));
promise2.catch(err => console.error(err));

ただし、非同期コールバック内でのthrowは自動的に捕捉されません。例えば、setTimeoutやsetInterval内でthrowされたエラーは、Promise自体には伝播しないため注意が必要です:

// これは正しく捕捉されない例
const badPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error('このエラーは捕捉されない');
  }, 100);
});

// この場合は明示的にrejectを使う
const goodPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('このエラーは捕捉される'));
  }, 100);
});

エラー処理の条件分岐方法

Promiseのエラーハンドリングでは、エラーの種類に応じて異なる処理を実行することがよくあります。catch()メソッド内で条件分岐を使うことで、柔軟なエラー処理が可能になります。

エラータイプによる分岐の基本的な実装例:

fetchData()
  .then(data => processData(data))
  .catch(error => {
    if (error instanceof NetworkError) {
      console.error('ネットワークエラー:', error.message);
      // ネットワークエラー専用の処理
      return retryRequest();
    } else if (error instanceof ValidationError) {
      console.error('バリデーションエラー:', error.message);
      // バリデーションエラー専用の処理
      return handleValidationError(error);
    } else if (error.code === 'TIMEOUT') {
      console.error('タイムアウトエラー');
      // タイムアウト専用の処理
      return handleTimeout();
    } else {
      console.error('予期しないエラー:', error);
      // その他のエラー処理
      throw error; // エラーを再スロー
    }
  });

カスタムエラークラスを定義することで、より明確なエラー分類が可能になります:

class NetworkError extends Error {
  constructor(message) {
    super(message);
    this.name = 'NetworkError';
  }
}

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

function validateUser(user) {
  return new Promise((resolve, reject) => {
    if (!user.email) {
      reject(new ValidationError('メールアドレスが必要です', 'email'));
    } else if (!user.name) {
      reject(new ValidationError('名前が必要です', 'name'));
    } else {
      resolve(user);
    }
  });
}

複数のcatch()を連鎖させることで、段階的なエラー処理も実装できます:

apiRequest()
  .catch(error => {
    // 最初のcatchで特定のエラーのみ処理
    if (error.code === 'RETRY_POSSIBLE') {
      return retryRequest(); // リトライして新しいPromiseを返す
    }
    throw error; // その他のエラーは次のcatchへ
  })
  .catch(error => {
    // 2番目のcatchで残りのエラーを処理
    console.error('最終的なエラー処理:', error);
    return defaultValue; // デフォルト値を返す
  });

uncaughtExceptionとunhandledRejectionの違い

Node.js環境では、Promiseに関連する2つの重要なグローバルエラーイベントがあります。これらを理解することで、本番環境でのエラー監視と対応が適切に行えます。

uncaughtExceptionは、同期的なコードで発生したキャッチされていない例外を捕捉します:

process.on('uncaughtException', (error) => {
  console.error('キャッチされていない例外:', error);
  // アプリケーションは不安定な状態になる可能性がある
  // ログを記録して終了することが推奨される
  process.exit(1);
});

// これがuncaughtExceptionを発生させる
throw new Error('同期的なエラー');

unhandledRejectionは、catch()やthen()の第二引数で処理されなかったPromiseのrejectionを捕捉します:

process.on('unhandledRejection', (reason, promise) => {
  console.error('ハンドリングされていないPromise rejection:', reason);
  console.error('Promise:', promise);
  // エラーログを記録
});

// これがunhandledRejectionを発生させる
Promise.reject(new Error('処理されないrejection'));

// catch()がないPromiseチェーン
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data));
// エラーが発生してもcatch()がないためunhandledRejection

両者の主な違いを表にまとめると:

項目 uncaughtException unhandledRejection
対象 同期的な例外 Promiseのrejection
発生タイミング throw文が実行された直後 イベントループの最後
アプリケーションの状態 不安定になる可能性が高い 比較的安定している
推奨される対応 ログを記録して終了 ログを記録して継続可能

Node.js v15以降では、unhandledRejectionがデフォルトでアプリケーションを終了させるようになりました。これにより、キャッチされないPromiseエラーの見落としを防ぐことができます:

// Node.js v15以降の推奨パターン
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // エラーログサービスに送信
  // 必要に応じてグレースフルシャットダウン
});

// 明示的にエラーハンドリングモードを設定
process.on('rejectionHandled', (promise) => {
  console.log('Promise rejection was handled:', promise);
});

Promiseのエラーを適切に補足する方法

Promiseのエラーを適切に捕捉することは、堅牢なアプリケーションを構築する上で不可欠です。ここでは、様々なシナリオにおけるベストプラクティスを紹介します。

基本的なcatch()の配置について、Promiseチェーンの最後には必ずcatch()を配置することが推奨されます:

// 良い例:最後にcatch()がある
fetchUserData(userId)
  .then(user => validateUser(user))
  .then(validUser => saveToDatabase(validUser))
  .then(result => console.log('保存成功:', result))
  .catch(error => {
    console.error('処理中にエラーが発生:', error);
    // エラー通知やログ記録
  });

// 悪い例:catch()がない
fetchUserData(userId)
  .then(user => validateUser(user))
  .then(validUser => saveToDatabase(validUser)); // エラーが補足されない

async/awaitを使用する場合は、try-catch文でラップします:

async function processUser(userId) {
  try {
    const user = await fetchUserData(userId);
    const validUser = await validateUser(user);
    const result = await saveToDatabase(validUser);
    console.log('保存成功:', result);
    return result;
  } catch (error) {
    console.error('処理中にエラーが発生:', error);
    // エラーハンドリング
    throw error; // 必要に応じて再スロー
  }
}

複数の非同期処理を並行実行する場合のエラーハンドリング:

// Promise.allを使う場合
async function fetchMultipleUsers(userIds) {
  try {
    const promises = userIds.map(id => fetchUserData(id));
    const users = await Promise.all(promises);
    return users;
  } catch (error) {
    // 一つでも失敗すると全体が失敗する
    console.error('ユーザーデータの取得に失敗:', error);
    throw error;
  }
}

// 個別にエラーハンドリングする場合
async function fetchMultipleUsersWithIndividualHandling(userIds) {
  const promises = userIds.map(id => 
    fetchUserData(id)
      .catch(error => {
        console.error(`ユーザーID ${id} の取得に失敗:`, error);
        return null; // エラー時はnullを返す
      })
  );
  
  const results = await Promise.all(promises);
  // nullを除外して成功したものだけを返す
  return results.filter(user => user !== null);
}

Promise.allSettledを使った堅牢なエラーハンドリング

async function fetchWithDetailedErrorHandling(urls) {
  const promises = urls.map(url => fetch(url));
  const results = await Promise.allSettled(promises);
  
  const successes = [];
  const failures = [];
  
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      successes.push({
        url: urls[index],
        data: result.value
      });
    } else {
      failures.push({
        url: urls[index],
        error: result.reason
      });
    }
  });
  
  if (failures.length > 0) {
    console.warn(`${failures.length}件のリクエストが失敗しました`);
    failures.forEach(failure => {
      console.error(`URL: ${failure.url}, エラー: ${failure.error}`);
    });
  }
  
  return { successes, failures };
}

最後に、エラーのラップと再スローのパターンも重要です:

class ApplicationError extends Error {
  constructor(message, originalError) {
    super(message);
    this.name = 'ApplicationError';
    this.originalError = originalError;
    this.timestamp = new Date();
  }
}

async function safeApiCall(endpoint) {
  try {
    const response = await fetch(endpoint);
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    // エラーをラップして詳細情報を追加
    throw new ApplicationError(
      `API呼び出しに失敗: ${endpoint}`,
      error
    );
  }
}

// 使用例
safeApiCall('https://api.example.com/data')
  .catch(error => {
    if (error instanceof ApplicationError) {
      console.error('アプリケーションエラー:', error.message);
      console.error('元のエラー:', error.originalError);
      console.error('発生時刻:', error.timestamp);
    }
  });

“`

“`html

Promiseの実践的な活用例

javascript+promise+async

JavaScriptのPromiseは、実際の開発現場でさまざまなシーンで活用されています。ここでは、Promiseを使った具体的な実装例を通じて、実践的な利用方法を見ていきましょう。これらの例を理解することで、実際のプロジェクトにおいてもPromiseを効果的に活用できるようになります。

Ajax通信での実装方法

Fetch APIを使用したAjax通信は、Promiseを活用する最も一般的なケースの一つです。Fetch APIはネイティブでPromiseを返すため、非同期通信を直感的に記述できます。

// Fetch APIを使った基本的なAjax通信
fetch('https://api.example.com/users')
  .then(response => {
    if (!response.ok) {
      throw new Error('ネットワークエラーが発生しました');
    }
    return response.json();
  })
  .then(data => {
    console.log('取得したデータ:', data);
    // データの処理を実行
  })
  .catch(error => {
    console.error('エラー:', error);
  });

// POSTリクエストの実装例
function createUser(userData) {
  return fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(userData)
  })
  .then(response => response.json())
  .then(data => {
    console.log('ユーザー作成完了:', data);
    return data;
  })
  .catch(error => {
    console.error('ユーザー作成に失敗しました:', error);
    throw error;
  });
}

// 使用例
createUser({ name: '山田太郎', email: 'yamada@example.com' })
  .then(user => {
    console.log('新しいユーザーID:', user.id);
  });

Fetch APIはPromiseベースで設計されているため、非同期処理の流れが読みやすく、エラーハンドリングもcatch()で一元管理できます。レスポンスのステータスコードをチェックし、適切なエラー処理を行うことで、堅牢なAjax通信を実装できます。

XMLHttpRequestによる非同期通信

従来のXMLHttpRequestはコールバックベースですが、Promiseでラップすることで、モダンな非同期処理として扱えるようになります。この手法は、古いブラウザへの対応が必要な場合や、Fetch APIが使えない環境で有効です。

// XMLHttpRequestをPromise化する関数
function httpRequest(url, method = 'GET', data = null) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    
    xhr.open(method, url);
    xhr.setRequestHeader('Content-Type', 'application/json');
    
    xhr.onload = function() {
      if (xhr.status >= 200 && xhr.status  300) {
        try {
          const response = JSON.parse(xhr.responseText);
          resolve(response);
        } catch (e) {
          resolve(xhr.responseText);
        }
      } else {
        reject(new Error(`HTTP Error: ${xhr.status}`));
      }
    };
    
    xhr.onerror = function() {
      reject(new Error('ネットワークエラーが発生しました'));
    };
    
    xhr.ontimeout = function() {
      reject(new Error('リクエストがタイムアウトしました'));
    };
    
    xhr.timeout = 5000; // 5秒のタイムアウト
    
    if (data) {
      xhr.send(JSON.stringify(data));
    } else {
      xhr.send();
    }
  });
}

// 使用例
httpRequest('https://api.example.com/data')
  .then(data => {
    console.log('データ取得成功:', data);
    return httpRequest('https://api.example.com/related', 'POST', data);
  })
  .then(result => {
    console.log('関連データ登録完了:', result);
  })
  .catch(error => {
    console.error('処理に失敗しました:', error.message);
  });

この実装により、XMLHttpRequestの柔軟性を保ちながら、Promiseチェーンによる可読性の高いコードを書くことができます。onload、onerror、ontimeoutといった各種イベントハンドラを適切に設定することで、様々なエラーケースに対応できます。

画像の非同期読み込み処理

画像の読み込みもPromiseを活用することで、読み込み完了やエラーを効率的に制御できます。複数の画像を順次または並行で読み込む際に特に有効です。

// 画像読み込みをPromise化する関数
function loadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    
    img.onload = function() {
      resolve(img);
    };
    
    img.onerror = function() {
      reject(new Error(`画像の読み込みに失敗しました: ${url}`));
    };
    
    img.src = url;
  });
}

// 単一画像の読み込み例
loadImage('https://example.com/image1.jpg')
  .then(img => {
    document.body.appendChild(img);
    console.log('画像を表示しました');
  })
  .catch(error => {
    console.error(error.message);
  });

// 複数画像を並行で読み込む例
const imageUrls = [
  'https://example.com/image1.jpg',
  'https://example.com/image2.jpg',
  'https://example.com/image3.jpg'
];

Promise.all(imageUrls.map(url => loadImage(url)))
  .then(images => {
    console.log(`${images.length}枚の画像を読み込みました`);
    images.forEach(img => {
      document.getElementById('gallery').appendChild(img);
    });
  })
  .catch(error => {
    console.error('一部の画像の読み込みに失敗しました:', error);
  });

// フォールバック画像を使った堅牢な実装
function loadImageWithFallback(url, fallbackUrl) {
  return loadImage(url)
    .catch(error => {
      console.warn('メイン画像の読み込みに失敗、フォールバック画像を使用します');
      return loadImage(fallbackUrl);
    });
}

loadImageWithFallback(
  'https://example.com/main.jpg',
  'https://example.com/fallback.jpg'
)
.then(img => {
  document.body.appendChild(img);
});

画像の読み込みをPromise化することで、ローディング表示やプログレスバーの実装が容易になり、ユーザー体験を向上させることができます。Promise.all()を使えば複数の画像を並行で読み込み、すべての読み込みが完了した時点で処理を実行できます。

タイムアウト処理の実装

非同期処理に時間制限を設けたい場合、Promiseを使ったタイムアウト処理が有効です。長時間応答がない場合に処理を中断し、適切なエラーハンドリングを行うことができます。

// タイムアウト機能を持つPromiseを生成する関数
function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error(`タイムアウト: ${ms}ms以内に処理が完了しませんでした`));
    }, ms);
  });
}

// Promiseにタイムアウトを設定する関数
function promiseWithTimeout(promise, ms) {
  return Promise.race([
    promise,
    timeout(ms)
  ]);
}

// 使用例:Fetch APIにタイムアウトを設定
promiseWithTimeout(
  fetch('https://api.example.com/slow-endpoint'),
  3000 // 3秒のタイムアウト
)
  .then(response => response.json())
  .then(data => {
    console.log('データ取得成功:', data);
  })
  .catch(error => {
    console.error('エラー:', error.message);
  });

// 遅延実行を行うdelay関数
function delay(ms, value) {
  return new Promise(resolve => {
    setTimeout(() => resolve(value), ms);
  });
}

// リトライ機能付きタイムアウト処理
async function fetchWithRetry(url, retries = 3, timeoutMs = 5000) {
  for (let i = 0; i  retries; i++) {
    try {
      const response = await promiseWithTimeout(fetch(url), timeoutMs);
      return await response.json();
    } catch (error) {
      console.warn(`試行 ${i + 1}/${retries} 失敗:`, error.message);
      if (i === retries - 1) {
        throw new Error(`${retries}回の試行後も失敗しました`);
      }
      await delay(1000 * (i + 1)); // 指数バックオフ的な遅延
    }
  }
}

// 使用例
fetchWithRetry('https://api.example.com/data', 3, 3000)
  .then(data => {
    console.log('データ取得成功:', data);
  })
  .catch(error => {
    console.error('最終的に失敗しました:', error.message);
  });

タイムアウト処理を実装しないと、ネットワークの問題などで処理が無限に待機してしまう可能性があるため注意が必要です。Promise.race()を活用することで、元のPromiseとタイムアウト用のPromiseを競争させ、先に完了した方の結果を採用できます。この手法は、ユーザーが長時間待たされることを防ぎ、適切なフィードバックを提供するために重要です。

これらの実践例を参考にすることで、JavaScriptのPromiseを使った堅牢で保守性の高い非同期処理を実装できるようになります。実際の開発では、これらのパターンを組み合わせて、プロジェクトの要件に合った最適な実装を選択しましょう。

“`

“`html

Promiseの実装パターンとベストプラクティス

javascript+promise+async

JavaScriptのPromiseを実際のプロジェクトで活用する際には、基本的な文法を理解するだけでなく、効率的な実装パターンやベストプラクティスを知っておくことが重要です。複雑な非同期処理を扱う場面では、適切な実装パターンを選択することでコードの保守性や可読性を大きく向上させることができます。このセクションでは、実務で役立つPromiseの実装パターンと、品質の高いコードを書くためのベストプラクティスを解説します。

複数の非同期処理を効率的に扱う方法

実際の開発現場では、複数の非同期処理を同時に扱う必要がある場面が頻繁に発生します。例えば、複数のAPIエンドポイントからデータを取得する場合や、並行してファイル読み込みを行う場合などです。このような状況では、処理の依存関係や実行順序を考慮した実装パターンを選択することが求められます。

並列実行が可能な場合はPromise.all()を活用することで、複数の非同期処理を同時に開始し、全ての処理が完了するのを効率的に待機できます。以下は複数のAPIリクエストを並列実行する例です。

const fetchUserData = async () => {
  const [userData, postsData, commentsData] = await Promise.all([
    fetch('/api/user/123').then(res => res.json()),
    fetch('/api/posts').then(res => res.json()),
    fetch('/api/comments').then(res => res.json())
  ]);
  
  return { userData, postsData, commentsData };
};

一方で、処理に依存関係がある場合は、逐次実行パターンを採用する必要があります。例えば、ユーザー情報を取得してから、そのユーザーIDを使って関連データを取得する場合などです。

const fetchRelatedData = async () => {
  const user = await fetch('/api/user').then(res => res.json());
  const posts = await fetch(`/api/posts/${user.id}`).then(res => res.json());
  const details = await fetch(`/api/details/${posts[0].id}`).then(res => res.json());
  
  return { user, posts, details };
};

部分的な失敗を許容する場合は、Promise.allSettled()を使用することで、一部の処理が失敗しても他の処理結果を取得できます。これは複数のサードパーティAPIを呼び出す際に特に有効です。

const fetchMultipleSources = async () => {
  const results = await Promise.allSettled([
    fetch('/api/source1').then(res => res.json()),
    fetch('/api/source2').then(res => res.json()),
    fetch('/api/source3').then(res => res.json())
  ]);
  
  return results
    .filter(result => result.status === 'fulfilled')
    .map(result => result.value);
};

Promiseの結果を変数に格納する手法

Promiseの実行結果を適切に変数に格納し、後続の処理で利用することは、コードの可読性と保守性を高める上で重要なポイントです。特に複数のPromiseの結果を組み合わせて処理する場合、変数への格納方法によってコードの明確さが大きく変わります。

最もシンプルな方法は、async/awaitを使用して非同期処理の結果を直接変数に代入する方法です。この方法は同期処理と同様の記述スタイルとなり、理解しやすいコードになります。

const processData = async () => {
  const response = await fetch('/api/data');
  const data = await response.json();
  const processedData = data.map(item => item.value * 2);
  
  return processedData;
};

複数の非同期処理結果を格納する場合、分割代入を活用することでコードを簡潔に記述できます。Promise.all()と組み合わせることで、複数の値を一度に変数へ格納可能です。

const loadResources = async () => {
  const [config, userData, settingsData] = await Promise.all([
    fetch('/api/config').then(res => res.json()),
    fetch('/api/user').then(res => res.json()),
    fetch('/api/settings').then(res => res.json())
  ]);
  
  // 取得した各データを使用した処理
  return {
    config,
    user: userData,
    settings: settingsData
  };
};

オブジェクトの分割代入を使用すると、さらに柔軟な変数格納が可能になります。特に、APIレスポンスから必要なプロパティのみを抽出する場合に有効です。

const getUserInfo = async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  const { name, email, profile: { age, country } } = await response.json();
  
  return { name, email, age, country };
};

Promiseの結果を変数に格納する際は、エラーハンドリングを忘れないよう注意してください。try-catch文で適切にラップすることで、予期しないエラーによるアプリケーションのクラッシュを防ぐことができます。

const safeDataFetch = async () => {
  let data = null;
  let error = null;
  
  try {
    const response = await fetch('/api/data');
    data = await response.json();
  } catch (err) {
    error = err.message;
  }
  
  return { data, error };
};

ミドルウェアでの例外補足の実装

Expressなどのフレームワークを使用したサーバーサイドのJavaScript開発では、ミドルウェアでPromiseの例外を適切に補足することが重要です。非同期処理中に発生したエラーを適切にハンドリングしないと、サーバーがクラッシュしたり、クライアントに適切なエラーレスポンスを返せなくなったりします。

Expressのミドルウェアでは、async/awaitを使用した非同期処理のエラーをnext()関数に渡すことでエラーハンドリングミドルウェアへ処理を委譲できます。以下は基本的なパターンです。

// 非同期ミドルウェアのラッパー関数
const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

// 使用例
app.get('/api/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  
  if (!user) {
    throw new Error('User not found');
  }
  
  res.json(user);
}));

より詳細なエラーハンドリングが必要な場合は、カスタムエラークラスを定義し、エラーの種類に応じた適切なレスポンスを返す実装が推奨されます。

// カスタムエラークラス
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

// エラーハンドリングミドルウェア
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.isOperational ? err.message : 'Internal Server Error';
  
  res.status(statusCode).json({
    status: 'error',
    statusCode,
    message
  });
});

複数の非同期処理を伴うミドルウェアでは、各処理でのエラーを適切に伝播させることが重要です。Promise.all()などを使用する場合、個別のエラーハンドリングと全体のエラーハンドリングを組み合わせることで、堅牢な実装が可能になります。

app.post('/api/batch-update', asyncHandler(async (req, res) => {
  const { items } = req.body;
  
  try {
    const results = await Promise.allSettled(
      items.map(item => updateItem(item))
    );
    
    const succeeded = results.filter(r => r.status === 'fulfilled');
    const failed = results.filter(r => r.status === 'rejected');
    
    if (failed.length > 0) {
      return res.status(207).json({
        message: 'Partial success',
        succeeded: succeeded.length,
        failed: failed.length,
        errors: failed.map(f => f.reason.message)
      });
    }
    
    res.json({
      message: 'All updates successful',
      count: succeeded.length
    });
  } catch (error) {
    throw new AppError('Batch update failed', 500);
  }
}));

ミドルウェアでの例外補足では、必ずnext()関数を呼び出すか、レスポンスを返すかのいずれかを実行してください。これを怠ると、リクエストがハングアップしてしまう原因となります。

“`

“`html

Promiseを使う際の注意点とトラブルシューティング

javascript+promise+coding

JavaScriptのPromiseは非常に強力な機能ですが、正しく理解して使用しないと予期しない動作やエラーに遭遇することがあります。ここでは、実際の開発現場でよく遭遇する注意点と、トラブルが発生した際の解決方法について解説します。これらのポイントを押さえることで、より安全で保守性の高いコードを書くことができるようになります。

await式の使用可能な場所

await式は非同期処理を同期処理のように記述できる便利な構文ですが、使用できる場所には明確な制限があります。最も重要なルールは、await式はasync functionの内部でのみ使用可能という点です。

// NG: トップレベルでawaitを使用(通常のスクリプトでは不可)
const result = await fetchData();

// OK: async function内で使用
async function getData() {
  const result = await fetchData();
  return result;
}

ただし、ES2022以降ではモジュールシステム(type=”module”)を使用している場合に限り、トップレベルでのawait(Top-level await)が利用可能になりました。これにより、モジュールの読み込み時に非同期処理の完了を待つことができます。

// モジュール内であればトップレベルawaitが可能
// script type="module"の場合
const config = await fetch('/api/config').then(res => res.json());
export default config;

また、通常の関数内でawaitを使用しようとすると、以下のようなエラーが発生します。

function normalFunction() {
  const data = await fetchData(); // SyntaxError
}

このような場合は、関数定義にasyncキーワードを追加する必要があります。コールバック関数内でawaitを使用する際も同様に、コールバック関数自体をasync functionとして定義する必要があることに注意してください。

Promiseの再解決について

Promiseの重要な特性として、一度解決(resolve)または拒否(reject)されたPromiseは、その状態を変更できないという点があります。これは「Promiseは一度だけ決定される」という原則で、JavaScriptエンジンによって厳格に守られています。

const promise = new Promise((resolve, reject) => {
  resolve('最初の解決');
  resolve('2回目の解決'); // この呼び出しは無視される
  reject('エラー'); // この呼び出しも無視される
});

promise.then(value => {
  console.log(value); // '最初の解決'のみが出力される
});

この仕様により、Promiseの結果は予測可能で信頼性の高いものになりますが、初心者がよく混乱するポイントでもあります。特に以下のようなケースでは注意が必要です。

  • タイマー処理とAPI呼び出しを組み合わせた場合の競合状態
  • 複数の条件分岐でresolve/rejectを呼び出す場合
  • イベントリスナー内でPromiseを解決しようとする場合
// 誤った使用例:複数回解決しようとするケース
function unreliablePromise() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('タイムアウト'), 1000);
    
    fetch('/api/data')
      .then(data => resolve(data)) // タイムアウトが先に発生した場合、これは無視される
      .catch(err => reject(err));
  });
}

複数の非同期処理のうち、最初に完了したものを採用したい場合は、Promise.race()を使用するのが適切です。また、すべての処理の結果を取得したい場合はPromise.allSettled()を活用しましょう。

よくあるエラーと対処法

Promiseを使用する際には、いくつかの典型的なエラーパターンがあります。これらを理解しておくことで、トラブルシューティングを迅速に行うことができます。

Uncaught (in promise)エラー

最も頻繁に遭遇するエラーの一つが「Uncaught (in promise)」です。これは、Promise内で発生したエラーがcatchされていない場合に発生します。

// NG: エラーハンドリングがない
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data));
// ネットワークエラーが発生すると「Uncaught (in promise)」エラーになる

// OK: catchでエラーをハンドリング
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('エラーが発生しました:', error));

Promise内でのreturn忘れ

Promiseチェーンで次の処理に値を渡す際、returnを忘れるとundefinedが渡されてしまいます。

// NG: returnを忘れている
fetch('/api/user')
  .then(response => {
    response.json(); // returnがない!
  })
  .then(data => {
    console.log(data); // undefined が出力される
  });

// OK: 正しくreturnする
fetch('/api/user')
  .then(response => {
    return response.json();
  })
  .then(data => {
    console.log(data); // 正しいデータが出力される
  });

async functionの戻り値の誤解

async functionは常にPromiseを返します。この特性を理解していないと、意図しない動作になることがあります。

async function getUserData() {
  return { name: 'Taro', age: 25 };
}

// NG: 直接値を取得しようとする
const user = getUserData();
console.log(user.name); // undefined(userはPromiseオブジェクト)

// OK: awaitまたはthenで値を取得
const user = await getUserData();
console.log(user.name); // 'Taro'

並行処理と逐次処理の混同

複数の非同期処理を扱う際、並行実行と逐次実行を混同すると、パフォーマンスや動作に問題が生じることがあります。

// 逐次処理(遅い)
async function sequential() {
  const result1 = await fetch('/api/data1');
  const result2 = await fetch('/api/data2'); // result1の完了を待ってから実行
  return [result1, result2];
}

// 並行処理(速い)
async function parallel() {
  const [result1, result2] = await Promise.all([
    fetch('/api/data1'),
    fetch('/api/data2') // 同時に実行される
  ]);
  return [result1, result2];
}

これらのエラーパターンを理解し、適切なエラーハンドリングとコード構造を心がけることで、より堅牢で保守性の高いPromiseベースのコードを書くことができます。デバッグ時にはブラウザの開発者ツールのコンソールとネットワークタブを活用し、Promiseの状態遷移を追跡することも効果的です。

“`