0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript】Promiseを理解する - 非同期処理をスマートに扱う

0
Posted at

はじめに

Promiseとは何か

Promiseは、JavaScriptで非同期処理を扱うための仕組みです。「約束」という名前の通り、「将来的に値が返ってくることを約束するオブジェクト」として機能します。

非同期処理とは、処理の完了を待たずに次の処理を実行できる仕組みのことです。例えば、サーバーからデータを取得する際、データが返ってくるまで待つ必要がありますが、その間に他の処理を進めることができます。

なぜPromiseが必要なのか

従来のコールバック関数だけでは、複数の非同期処理を連続して実行する際にコードが複雑になってしまいます。Promiseを使うことで、非同期処理をより直感的で読みやすいコードで書けるようになります。

非同期処理の基礎知識

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

同期処理は、1つの処理が完了してから次の処理に進む方式です。一方、非同期処理は処理の完了を待たずに次の処理を進められます。

// 同期処理の例
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つ目の順

コールバック関数の限界

コールバック関数を使った非同期処理では、複数の処理を連続して実行すると、ネストが深くなり読みにくくなります。これを「コールバック地獄」と呼びます。

// コールバック地獄の例
getData1(function(result1) {
  getData2(result1, function(result2) {
    getData3(result2, function(result3) {
      getData4(result3, function(result4) {
        // さらにネストが続く...
      });
    });
  });
});

Promiseの基本

Promiseの構文

Promiseは以下のように作成します。

const promise = new Promise((resolve, reject) => {
  // 非同期処理
  const success = true;
  
  if (success) {
    resolve('成功しました');
  } else {
    reject('失敗しました');
  }
});

Promiseのコンストラクタには、2つの引数を持つ関数を渡します。

  • resolve: 処理が成功した際に呼び出す関数
  • reject: 処理が失敗した際に呼び出す関数

Promiseの3つの状態

Promiseは常に以下の3つの状態のいずれかを持ちます。

  • Pending(待機中): 初期状態。処理がまだ完了していない
  • Fulfilled(成功): 処理が正常に完了した
  • Rejected(拒否): 処理が失敗した

resolveとrejectの役割

resolveは処理が成功した際に結果を返すために使います。rejectは処理が失敗した際にエラー情報を返すために使います。

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    if (userId) {
      // ユーザーデータ取得に成功
      resolve({ id: userId, name: 'Taro' });
    } else {
      // ユーザーIDが無効
      reject('ユーザーIDが指定されていません');
    }
  });
}

Promiseの使い方

.then()メソッドで成功時の処理

Promiseが成功した際の処理は.then()メソッドで記述します。

fetchUserData(1)
  .then((userData) => {
    console.log('ユーザー名:', userData.name);
  });

.then()メソッドは、resolveに渡された値を引数として受け取ります。

.catch()メソッドでエラーハンドリング

Promiseが失敗した際の処理は.catch()メソッドで記述します。

fetchUserData(null)
  .catch((error) => {
    console.error('エラー:', error);
  });

.finally()メソッドで共通処理

成功・失敗に関わらず実行したい処理は.finally()メソッドで記述します。

fetchUserData(1)
  .then((userData) => {
    console.log('成功:', userData);
  })
  .catch((error) => {
    console.error('失敗:', error);
  })
  .finally(() => {
    console.log('処理が完了しました');
  });

Promiseのチェーン

メソッドチェーンの書き方

.then()メソッドは新しいPromiseを返すため、連続して記述できます。

fetchUserData(1)
  .then((userData) => {
    console.log('ユーザー取得:', userData.name);
    return userData.id;
  })
  .then((userId) => {
    console.log('ユーザーID:', userId);
    return userId * 2;
  })
  .then((result) => {
    console.log('計算結果:', result);
  })
  .catch((error) => {
    console.error('エラー:', error);
  });

複数の非同期処理を順番に実行する

Promiseチェーンを使えば、複数の非同期処理を順番に実行できます。

function getUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: userId, name: 'Taro' });
    }, 1000);
  });
}

function getPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([{ id: 1, title: '記事1' }, { id: 2, title: '記事2' }]);
    }, 1000);
  });
}

getUser(1)
  .then((user) => {
    console.log('ユーザー:', user.name);
    return getPosts(user.id);
  })
  .then((posts) => {
    console.log('投稿数:', posts.length);
  })
  .catch((error) => {
    console.error('エラー:', error);
  });

Promiseの便利なメソッド

Promise.all() - 複数のPromiseを並列実行

Promise.all()は、複数のPromiseを並列で実行し、すべてが完了するまで待ちます。すべてのPromiseが成功した場合のみ、結果の配列を返します。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve) => setTimeout(() => resolve('foo'), 100));
const promise3 = Promise.resolve(42);

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // [3, 'foo', 42]
  })
  .catch((error) => {
    console.error('いずれかのPromiseが失敗:', error);
  });

1つでもPromiseが失敗すると、Promise.all()全体が失敗します。

Promise.race() - 最初に完了したPromiseを取得

Promise.race()は、最初に完了したPromiseの結果を返します。

const promise1 = new Promise((resolve) => setTimeout(() => resolve('1番'), 500));
const promise2 = new Promise((resolve) => setTimeout(() => resolve('2番'), 100));

Promise.race([promise1, promise2])
  .then((value) => {
    console.log(value); // '2番'
  });

タイムアウト処理の実装などに使えます。

Promise.allSettled() - すべての結果を取得

Promise.allSettled()は、すべてのPromiseが完了するまで待ち、成功・失敗に関わらずすべての結果を返します。

const promise1 = Promise.resolve(3);
const promise2 = Promise.reject('エラー');
const promise3 = Promise.resolve(42);

Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    results.forEach((result) => {
      console.log(result.status); // 'fulfilled' または 'rejected'
    });
  });

Promise.any() - 最初に成功したPromiseを取得

Promise.any()は、最初に成功したPromiseの結果を返します。すべてが失敗した場合のみエラーになります。

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

Promise.any([promise1, promise2, promise3])
  .then((value) => {
    console.log(value); // '成功'
  });

async/awaitとの関係

async/awaitの基本

async/awaitは、Promiseをより読みやすく書くための構文です。asyncキーワードを関数に付けると、その関数は自動的にPromiseを返します。

async function fetchData() {
  return 'データ';
}

// 上記は以下と同じ
function fetchData() {
  return Promise.resolve('データ');
}

awaitキーワードは、Promiseの結果が返ってくるまで処理を待ちます。awaitはasync関数の中でのみ使用できます。

async function getUserAndPosts() {
  try {
    const user = await getUser(1);
    console.log('ユーザー:', user.name);
    
    const posts = await getPosts(user.id);
    console.log('投稿数:', posts.length);
  } catch (error) {
    console.error('エラー:', error);
  }
}

PromiseとAsync/Awaitの使い分け

Promiseチェーンとasync/awaitは、状況に応じて使い分けましょう。

Promiseチェーンが適している場合

  • 複数の非同期処理を並列実行したい
  • Promise.all()などの静的メソッドを使う

async/awaitが適している場合

  • 順次実行する処理が多い
  • 同期処理のように読みやすく書きたい
  • エラーハンドリングをtry-catchで統一したい
// Promiseチェーン
function processData() {
  return fetchUser()
    .then(user => fetchPosts(user.id))
    .then(posts => processPosts(posts))
    .catch(error => handleError(error));
}

// async/await
async function processData() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    return await processPosts(posts);
  } catch (error) {
    handleError(error);
  }
}

実践例

API通信での利用例

実際のAPI通信では、fetchメソッドがPromiseを返すため、Promiseの理解が重要です。

async function fetchUserFromAPI(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    
    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }
    
    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error('API通信エラー:', error);
    throw error;
  }
}

// 使用例
fetchUserFromAPI(1)
  .then(user => {
    console.log('ユーザー情報:', user);
  })
  .catch(error => {
    console.error('処理失敗:', error);
  });

エラーハンドリングのベストプラクティス

Promiseを使う際は、適切なエラーハンドリングが重要です。

function robustFetch(url) {
  return fetch(url)
    .then(response => {
      // HTTPステータスのチェック
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      return response.json();
    })
    .then(data => {
      // データの検証
      if (!data || typeof data !== 'object') {
        throw new Error('無効なデータ形式');
      }
      return data;
    })
    .catch(error => {
      // エラーの種類に応じた処理
      if (error.name === 'TypeError') {
        console.error('ネットワークエラー:', error.message);
      } else {
        console.error('その他のエラー:', error.message);
      }
      throw error; // 呼び出し元でもハンドリングできるよう再スロー
    });
}

まとめ

Promiseを使うメリット

Promiseを使うことで、以下のメリットが得られます。

  • 非同期処理を直感的に記述できる
  • エラーハンドリングが統一的に行える
  • コールバック地獄を回避できる
  • 複数の非同期処理を柔軟に組み合わせられる
0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?