Node.jsのPromiseを学ぶ

Promiseとは、JavaScriptにおける特別なオブジェクトで、非同期処理の最終的な完了(または失敗)とその結果の値を表します。本質的に、Promiseはまだ利用できないが将来利用可能になる値のプレースホルダーです。

Promiseをピザの注文に例えてみましょう。すぐには届きませんが、配達員は後で届けることを約束してくれます。いつ届くかは正確にはわかりませんが、結果は「ピザが配達された」か「何か問題が発生した」かのどちらかになります。

Promiseの状態

Promiseには3つの状態があります

  • Pending(保留中): 初期状態で、非同期処理がまだ実行中です。
  • Fulfilled(履行済み): 処理が成功裏に完了し、Promiseが値とともに解決(resolve)された状態です。
  • Rejected(拒否済み): 処理が失敗し、Promiseが理由(通常はエラー)とともに確定(settle)された状態です。

ピザを注文したとき、あなたは保留中の状態で、お腹を空かせながら期待しています。ピザが熱々でチーズたっぷりの状態で届けば、履行済みの状態になります。しかし、レストランから電話があり、ピザを床に落としてしまったと言われたら、拒否済みの状態になります。

夕食が喜びで終わるか失望で終わるかにかかわらず、最終的な結果が出た時点で、Promiseはsettled(確定済み)と見なされます。

Promiseの基本的な構文

Promiseを作成する最も一般的な方法の一つは、new Promise()コンストラクタを使用することです。このコンストラクタは、resolverejectという2つのパラメータを持つ関数を受け取ります。これらの関数は、Promiseの状態を保留中から履行済みまたは拒否済みに遷移させるために使用されます。

executor関数内でエラーがスローされた場合、Promiseはそのエラーで拒否されます。executor関数の戻り値は無視されます。Promiseを確定させるためには、resolveまたはrejectのみを使用する必要があります。

const  = new ((, ) => {
  const  = true;

  if () {
    ('Operation was successful!');
  } else {
    ('Something went wrong.');
  }
});

上記の例では

  • success条件がtrueの場合、Promiseは履行され、値'Operation was successful!'resolve関数に渡されます。
  • success条件がfalseの場合、Promiseは拒否され、エラー'Something went wrong.'reject関数に渡されます。

.then().catch().finally()によるPromiseのハンドリング

Promiseが作成されると、.then().catch().finally()メソッドを使用してその結果を処理できます。

  • .then()は、履行されたPromiseを処理し、その結果にアクセスするために使用されます。
  • .catch()は、拒否されたPromiseを処理し、発生する可能性のあるエラーをキャッチするために使用されます。
  • .finally()は、Promiseが解決されたか拒否されたかにかかわらず、確定したPromiseを処理するために使用されます。
const  = new ((, ) => {
  const  = true;

  if () {
    ('Operation was successful!');
  } else {
    ('Something went wrong.');
  }
});


  .( => {
    .(); // This will run if the Promise is fulfilled
  })
  .( => {
    .(); // This will run if the Promise is rejected
  })
  .(() => {
    .('The promise has completed'); // This will run when the Promise is settled
  });

Promiseのチェーン

Promiseの優れた機能の一つは、複数の非同期処理を連結(チェーン)できることです。Promiseをチェーンすると、各.then()ブロックは前のブロックが完了するのを待ってから実行されます。

const { :  } = ('node:timers/promises');

const  = (1000).(() => 'First task completed');


  .( => {
    .(); // 'First task completed'
    return (1000).(() => 'Second task completed'); // Return a second Promise
  })
  .( => {
    .(); // 'Second task completed'
  })
  .( => {
    .(); // If any Promise is rejected, catch the error
  });

PromiseとAsync/Awaitの使用

現代のJavaScriptでPromiseを扱う最良の方法の一つは、async/awaitを使用することです。これにより、同期的に見える非同期コードを書くことができ、読みやすく保守しやすくなります。

  • asyncは、Promiseを返す関数を定義するために使用されます。
  • awaitは、async関数内でPromiseが確定するまで実行を一時停止するために使用されます。
async function () {
  try {
    const  = await promise1;
    .(); // 'First task completed'

    const  = await promise2;
    .(); // 'Second task completed'
  } catch () {
    .(); // Catches any rejection or error
  }
}

();

performTasks関数では、awaitキーワードにより、各Promiseが確定するまで次のステートメントに進まないことが保証されます。これにより、非同期コードの流れがより直線的で読みやすくなります。

基本的に、上記のコードはユーザーが次のように書いた場合と同じように実行されます

promise1
  .then(function () {
    .();
    return promise2;
  })
  .then(function () {
    .();
  })
  .catch(function () {
    .();
  });

トップレベルAwait

ECMAScriptモジュールを使用する場合、モジュール自体が非同期操作をネイティブにサポートするトップレベルのスコープとして扱われます。これは、async関数を必要とせずにトップレベルでawaitを使用できることを意味します。

import {  as  } from 'node:timers/promises';

await (1000);

async/awaitは、提供された単純な例よりもはるかに複雑になることがあります。Node.js技術運営委員会のメンバーであるJames Snell氏による、Promisesとasync/awaitの複雑さを探る詳細なプレゼンテーションがあります。

PromiseベースのNode.js API

Node.jsは、多くのコアAPIのPromiseベースのバージョンを提供しており、特に非同期操作が伝統的にコールバックで処理されていた場合に顕著です。これにより、Node.jsのAPIとPromiseを扱いやすくなり、「コールバック地獄」のリスクが軽減されます。

たとえば、fs(ファイルシステム)モジュールには、fs.promises以下にPromiseベースのAPIがあります。

const  = ('node:fs').;
// Or, you can import the promisified version directly:
// const fs = require('node:fs/promises');

async function () {
  try {
    const  = await .('example.txt', 'utf8');
    .();
  } catch () {
    .('Error reading file:', );
  }
}

();

この例では、fs.readFile()がPromiseを返し、これをasync/await構文を使用して処理し、ファイルの内容を非同期に読み取ります。

高度なPromiseメソッド

JavaScriptのPromiseグローバルは、複数の非同期タスクをより効果的に管理するのに役立つ、いくつかの強力なメソッドを提供します。

Promise.all()

このメソッドはPromiseの配列を受け取り、すべてのPromiseが履行されると解決する新しいPromiseを返します。いずれかのPromiseが拒否されると、Promise.all()は即座に拒否されます。ただし、拒否が発生しても、Promiseは実行を続けます。多数のPromiseを処理する場合、特にバッチ処理では、この関数を使用するとシステムのメモリに負担がかかる可能性があります。

const { :  } = ('node:timers/promises');

const  = (1000).(() => 'Data from API 1');
const  = (2000).(() => 'Data from API 2');

.([, ])
  .( => {
    .(); // ['Data from API 1', 'Data from API 2']
  })
  .( => {
    .('Error:', );
  });

Promise.allSettled()

このメソッドは、すべてのPromiseが解決または拒否されるのを待ち、各Promiseの結果を記述するオブジェクトの配列を返します。

const  = .('Success');
const  = .('Failed');

.([, ]).( => {
  .();
  // [ { status: 'fulfilled', value: 'Success' }, { status: 'rejected', reason: 'Failed' } ]
});

Promise.all()とは異なり、Promise.allSettled()は失敗時にショートサーキットしません。一部が拒否されても、すべてのPromiseが確定するのを待ちます。これにより、失敗に関係なくすべてのタスクの状態を知りたいバッチ処理において、より優れたエラーハンドリングが可能になります。

Promise.race()

このメソッドは、最初のPromiseが解決または拒否されるとすぐに、解決または拒否します。どのPromiseが最初に確定したかに関わらず、すべてのPromiseは完全に実行されます。

const { :  } = ('node:timers/promises');

const  = (2000).(() => 'Task 1 done');
const  = (1000).(() => 'Task 2 done');

.([, ]).( => {
  .(); // 'Task 2 done' (since task2 finishes first)
});

Promise.any()

このメソッドは、Promiseのいずれか1つが解決するとすぐに解決します。すべてのPromiseが拒否された場合は、AggregateErrorで拒否されます。

const { :  } = ('node:timers/promises');

const  = (2000).(() => 'API 1 success');
const  = (1000).(() => 'API 2 success');
const  = (1500).(() => 'API 3 success');

.([, , ])
  .( => {
    .(); // 'API 2 success' (since it resolves first)
  })
  .( => {
    .('All promises rejected:', );
  });

Promise.reject()Promise.resolve()

これらのメソッドは、拒否された、または解決されたPromiseを直接作成します。

.('Resolved immediately').( => {
  .(); // 'Resolved immediately'
});

Promise.try()

Promise.try()は、同期か非同期かに関わらず、与えられた関数を実行し、その結果をPromiseでラップするメソッドです。関数がエラーをスローしたり、拒否されたPromiseを返したりした場合、Promise.try()は拒否されたPromiseを返します。関数が正常に完了した場合、返されるPromiseはその値で履行されます。

これは、特に同期的にエラーをスローする可能性のあるコードを扱う際に、一貫した方法でPromiseチェーンを開始するのに非常に役立ちます。

function () {
  if (.() > 0.5) {
    throw new ('Oops, something went wrong!');
  }
  return 'Success!';
}

.()
  .( => {
    .('Result:', );
  })
  .( => {
    .('Caught error:', .message);
  });

この例では、Promise.try()により、mightThrow()がエラーをスローした場合でも.catch()ブロックで捕捉されることが保証され、同期エラーと非同期エラーの両方を一箇所で簡単に処理できるようになります。

Promise.withResolvers()

このメソッドは、新しいPromiseとその関連するresolveおよびreject関数を作成し、それらを便利なオブジェクトで返します。これは、例えば、Promiseを作成する必要があるが、executor関数の外部から後で解決または拒否する場合に使用されます。

const { , ,  } = .();

(() => {
  ('Resolved successfully!');
}, 1000);

.( => {
  .('Success:', );
});

この例では、Promise.withResolvers()を使用することで、executor関数をインラインで定義する必要なく、Promiseがいつ、どのように解決または拒否されるかを完全に制御できます。このパターンは、イベント駆動型プログラミング、タイムアウト、またはPromiseベースでないAPIとの統合で一般的に使用されます。

Promiseのエラーハンドリング

Promiseでのエラーハンドリングは、予期せぬ状況でもアプリケーションが正しく動作することを保証します。

  • Promiseの実行中に発生したエラーや拒否を処理するために.catch()を使用できます。
myPromise
  .then( => .())
  .catch( => .()) // Handles the rejection
  .finally( => .('Promise completed')); // Runs regardless of promise resolution
  • あるいは、async/awaitを使用している場合は、try/catchブロックを使用してエラーをキャッチし、処理することができます。
async function () {
  try {
    const  = await myPromise;
    .();
  } catch () {
    .(); // Handles any errors
  } finally {
    // This code is executed regardless of failure
    .('performTask() completed');
  }
}

();

イベントループでのタスクのスケジューリング

Node.jsはPromiseに加えて、イベントループでタスクをスケジューリングするためのいくつかの他のメカニズムを提供します。

queueMicrotask()

queueMicrotask()は、マイクロタスクをスケジュールするために使用されます。マイクロタスクは、現在実行中のスクリプトの後、他のI/Oイベントやタイマーの前に実行される軽量のタスクです。マイクロタスクには、Promiseの解決や、通常のタスクよりも優先されるその他の非同期操作が含まれます。

(() => {
  .('Microtask is executed');
});

.('Synchronous task is executed');

上記の例では、「Microtask is executed」は「Synchronous task is executed」の後に、タイマーなどのI/O操作の前にログに出力されます。

process.nextTick()

process.nextTick()は、現在の操作が完了した直後に実行されるコールバックをスケジュールするために使用されます。これは、コールバックをできるだけ早く、しかし現在の実行コンテキストの後で実行させたい場合に便利です。

.(() => {
  .('Next tick callback');
});

.('Synchronous task executed');

setImmediate()

setImmediate()は、Node.jsのイベントループのチェックフェーズで実行されるコールバックをスケジュールします。これは、ほとんどのI/Oコールバックが処理されるポーリングフェーズの後に実行されます。

(() => {
  .('Immediate callback');
});

.('Synchronous task executed');

いつどれを使うか

  • queueMicrotask()は、現在のスクリプトの直後、かつI/Oやタイマーのコールバックの前に実行する必要があるタスク(通常はPromiseの解決)に使用します。
  • process.nextTick()は、I/Oイベントの前に実行すべきタスクに使用します。操作を遅延させたり、同期的にエラーを処理したりするのに役立ちます。
  • setImmediate()は、ポーリングフェーズの後、つまりほとんどのI/Oコールバックが処理された後に実行すべきタスクに使用します。

これらのタスクは現在の同期フローの外で実行されるため、これらのコールバック内でキャッチされなかった例外は、周囲のtry/catchブロックでは捕捉されず、適切に管理されない場合(例:Promiseに.catch()を付ける、process.on('uncaughtException')のようなグローバルエラーハンドラを使用するなど)、アプリケーションがクラッシュする可能性があります。

イベントループとさまざまなフェーズの実行順序についての詳細は、関連する記事「Node.jsのイベントループ」を参照してください。