NodeJS 速習チュートリアル

Node.js Promise

1. Promise 入門

Node.js におけるプロミス (Promise) は、従来のコールバック(Callback)と比較して、非同期処理(Asynchronous operations)をよりクリーンかつ直感的に扱うための手段を提供します。
プロミスは、非同期操作の完了(または失敗)と、その結果の値を表すオブジェクトです。

1.1 プロミスの状態 (Promise States)

  • Pending (ペンディング): 初期状態。操作が完了していない状態。
  • Fulfilled (フルフィルド): 操作が正常に完了した状態。
  • Rejected (リジェクテッド): 操作が失敗した状態。

一度プロミスがSettled (確定) 状態(フルフィルドまたはリジェクテッド)になると、その状態が変化することはありません。

1.2 プロミスを使用するメリット

コールバックの場合

// コールバックを使用したネストの深い構造
getUser(id, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);
    // 注文の処理...
  });
});

プロミスの場合

// プロミスによるフラットな記述
getUser(id)
  .then(user => getOrders(user.id))
  .then(orders => processOrders(orders))
  .catch(handleError);

主な利点:

  • コード構造がフラットになり、コールバック地獄 (Callback Hell) を回避できる。
  • 単一の .catch() による優れたエラーハンドリング。
  • 操作の合成やチェーン(連鎖)が容易。
  • 並列処理(Parallel operations)の標準サポート。

2. コールバック地獄の例(プロミスなし)

プロミスを使用しない場合、複数のファイルを順番に読み込むだけで以下のようにコードが複雑化します。

const fs = require('fs');

fs.readFile('file1.txt', (err, data1) => {
  if (err) throw err;
  fs.readFile('file2.txt', (err, data2) => {
    if (err) throw err;
    fs.readFile('file3.txt', (err, data3) => {
      if (err) throw err;
      // data1, data2, data3 を使用した処理
    });
  });
});

3. プロミスの作成と使用

プロミスは Promise コンストラクタを使用して作成できます。コンストラクタは、resolvereject という2つのパラメータを持つ実行関数(Executor function)を受け取ります。

3.1 基本的なプロミスの作成

// 新しいプロミスを作成
const myPromise = new Promise((resolve, reject) => {
  // 非同期操作をシミュレート (例: APIコール、ファイル読み込み)
  setTimeout(() => {
    const success = Math.random() > 0.5;
    
    if (success) {
      resolve('操作が正常に完了しました');
    } else {
      reject(new Error('操作が失敗しました'));
    }
  }, 1000); // 1秒の遅延をシミュレート
});

// プロミスを使用する
myPromise
  .then(result => console.log('成功:', result))
  .catch(error => console.error('エラー:', error.message));

3.2 例:プロミスを使用したファイルの読み取り

const fs = require('fs').promises;

const promise1 = Promise.resolve('最初の結果');
const promise2 = new Promise((resolve) => setTimeout(() => resolve('2番目の結果'), 1000));
const promise3 = fs.readFile('myfile.txt', 'utf8'); // ローカルファイルの読み取り

Promise.all([promise1, promise2, promise3])
  .then(results => {
    console.log('結果一覧:', results);
    // results[0] は promise1 から
    // results[1] は promise2 から
    // results[2] は myfile.txt の内容
  })
  .catch(error => {
    console.error('いずれかのプロミスでエラーが発生:', error);
  });

4. プロミスチェーン (Promise Chaining)

プロミスを連結することで、非同期操作を順番に実行できます。各 .then() は前の操作の結果を受け取ります。

function getUser(userId) {
  return new Promise((resolve, reject) => {
    // データベース呼び出しをシミュレート
    setTimeout(() => {
      resolve({ id: userId, name: 'John' });
    }, 1000);
  });
}

function getUserPosts(user) {
  return new Promise((resolve, reject) => {
    // API呼び出しをシミュレート
    setTimeout(() => {
      resolve(['投稿 1', '投稿 2', '投稿 3']);
    }, 1000);
  });
}

// プロミスをチェーンさせる
getUser(123)
  .then(user => {
    console.log('ユーザー:', user);
    return getUserPosts(user);
  })
  .then(posts => {
    console.log('投稿一覧:', posts);
  })
  .catch(error => {
    console.error('エラー:', error);
  });

5. プロミスのメソッド

5.1 インスタンスメソッド

  • then(onFulfilled, onRejected): 成功時または失敗時の処理をハンドルします。
  • catch(onRejected): 失敗(拒否)時の処理のみをハンドルします。
  • finally(onFinally): 成功・失敗に関わらず、確定した後に実行されます。

5.2 スタティックメソッド

  • Promise.all(iterable): すべてのプロミスが解決するまで待機します。
  • Promise.race(iterable): 最も早く確定したプロミスの結果を返します。
  • Promise.allSettled(iterable): すべてのプロミスが(成否に関わらず)確定するまで待機します。

5.3 ユーティリティメソッド

  • Promise.resolve(value): 解決済みのプロミスを作成します。
  • Promise.reject(reason): 拒否されたプロミスを作成します。

6. 並列実行のための Promise.all()

Promise.all() は、複数の非同期処理を並行して実行し、すべてが完了するのを待つのに使用されます。一つでも失敗すると即座にエラー(Fail-fast)となります。

const fs = require('fs').promises;
const promise1 = Promise.resolve('結果 1');
const promise2 = new Promise((resolve) => setTimeout(() => resolve('結果 2'), 1000));
const promise3 = fs.readFile('data.txt', 'utf8');

Promise.all([promise1, promise2, promise3])
  .then(results => {
    console.log('すべての結果:', results);
  })
  .catch(error => {
    console.error('いずれかの処理でエラー:', error);
  });

7. 最速の結果を得る Promise.race()

Promise.race() は、複数のプロミスのうち、成功・失敗を問わず最も早く結果が出たものを採用したい場合に便利です。

// タイムアウトパターンの例
const promise1 = new Promise(resolve => setTimeout(() => resolve('結果 1'), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve('結果 2'), 500));

Promise.race([promise1, promise2])
  .then(result => {
    console.log('最速の結果:', result);
    // promise2 の方が早いため '結果 2' がログに出力されます
  });

8. プロミスにおけるエラーハンドリング

適切なエラーハンドリングは開発において極めて重要です。プロミスでは主に以下の方法でエラーを処理します。

function fetchData() {
  return new Promise((resolve, reject) => {
    // ネットワークエラーのシミュレート
    reject(new Error('ネットワークエラーが発生しました'));
  });
}

// then の第2引数でハンドルする場合
fetchData()
  .then(
    data => console.log('データ:', data),
    error => console.log('then内でエラーをハンドル:', error.message)
  );

// catch を使用する推奨される方法
fetchData()
  .then(data => console.log('データ:', data))
  .catch(error => console.log('catch内でエラーをハンドル:', error.message));

ベストプラクティス: プロミスを使用する際は、必ず .catch() を含めるようにしましょう。これにより、予期せぬエラーによるプロミスの拒否(Unhandled Promise Rejection)を防ぎ、メモリリークやサイレントエラーを回避できます。