JavaScriptでawaitを使った非同期処理の最適化10例を実践

JavaScriptでawaitを使った非同期処理の最適化10例を実践 JS
この記事は約25分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればサンプルコードを活用して機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●awaitとは?非同期処理を極めるために知っておくべきこと

JavaScriptの非同期処理を極めるためには、awaitについて深く理解することが重要ですね。

awaitは非同期処理を同期的に書けるようにするための強力な機能ですが、使いこなすにはある程度の知識が必要です。

awaitを理解するためには、まずPromiseについて知っておく必要があります。

Promiseは非同期処理の結果を表すオブジェクトで、非同期処理の状態や結果値を保持します。

Promiseを使うことで、複雑な非同期処理をわかりやすく記述できるようになります。

○awaitを使う前に理解すべきPromiseの基本

Promiseは、次の3つの状態を持ちます。

  1. Pending(保留中) -> 非同期処理が完了していない状態
  2. Fulfilled(成功) -> 非同期処理が成功した状態
  3. Rejected(失敗) -> 非同期処理が失敗した状態

Promiseを使った非同期処理は、thenメソッドとcatchメソッドを使って結果を処理します。

thenメソッドは、Promiseが成功した時に呼ばれ、catchメソッドは失敗した時に呼ばれます。

○async/awaitの基本的な使い方

async/awaitは、Promiseをより簡潔に書くための構文です。

関数の前にasyncキーワードをつけることで、その関数は必ずPromiseを返すようになります。

また、awaitキーワードを使うことで、Promiseの結果が返されるまで処理を待機できます。

awaitを使うには、次のようなルールがあります。

  1. awaitは、async関数の中でのみ使用できる
  2. awaitの右辺には、Promiseを返す式を置く
  3. awaitを使うと、Promiseが解決(成功または失敗)されるまで処理が待機される

○サンプルコード1:Promiseを使った非同期処理

次のコードは、Promiseを使って非同期処理を行う例です。

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { id: 1, name: 'John Doe' };
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

このコードでは、fetchData関数がPromiseを返します。

Promiseの中では、1秒後にresolve関数が呼ばれ、データが返されます。

thenメソッドで成功時の処理を、catchメソッドで失敗時の処理を記述しています。

実行結果

{ id: 1, name: 'John Doe' }

○サンプルコード2:async/awaitを使った非同期処理

次のコードは、async/awaitを使って同じ処理を行う例です。

async function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { id: 1, name: 'John Doe' };
      resolve(data);
    }, 1000);
  });
}

async function main() {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

main();

このコードでは、fetchData関数はPromiseを返しますが、main関数はasyncキーワードがついているため、awaitを使ってPromiseの結果を待つことができます。

try...catch文を使って、エラーハンドリングも行っています。

実行結果

{ id: 1, name: 'John Doe' }

async/awaitを使うことで、Promiseのコードがより読みやすくなり、同期的な処理のように記述できるのがわかりますね。

これがawaitの基本的な使い方です。

awaitを使いこなすことで、JavaScriptの非同期処理をスマートに記述できるようになります。

次は、もっと実践的なawaitの使い方を見ていきましょう。

複数の非同期処理を順番に実行したり、並行して実行したりする方法や、エラーハンドリングの方法などを解説していきます。

●awaitを使った実践的な非同期処理の例

前章では、Promiseとasync/awaitの基本的な使い方を解説してきました。

この知識を踏まえて、より実践的なawaitの使用例を見ていきましょう。

非同期処理を極めるためには、様々なシチュエーションに対応できる柔軟性が求められます。

awaitを使いこなすことで、複数の非同期処理を順番に実行したり、並行して実行したりすることができます。

また、非同期処理の結果を配列で受け取ったり、エラーをキャッチしたりするテクニックも身につけておくと、より堅牢なコードを書けるようになります。

それでは、サンプルコードを交えながら、awaitの実践的な使い方を理解していきましょう。

きっと、非同期処理に対する理解が深まり、JavaScriptのスキルアップにつながるはずです。

○サンプルコード3:複数の非同期処理を順番に実行する

複数の非同期処理を順番に実行したい場合、awaitを使うと簡単に実現できます。

次のコードは、3つの非同期関数を順番に実行する例です。

async function task1() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Task 1 completed');
      resolve(1);
    }, 1000);
  });
}

async function task2() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Task 2 completed');
      resolve(2);
    }, 500);
  });
}

async function task3() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Task 3 completed');
      resolve(3);
    }, 750);
  });
}

async function main() {
  const result1 = await task1();
  const result2 = await task2();
  const result3 = await task3();
  console.log(result1, result2, result3);
}

main();

このコードでは、task1task2task3という3つの非同期関数を定義しています。

それぞれの関数は、一定の時間(1000ms、500ms、750ms)が経過した後に完了します。

main関数の中では、awaitを使って各タスクを順番に実行しています。

task1が完了するまで待ってからtask2を実行し、task2が完了するまで待ってからtask3を実行します。

最後に、各タスクの結果をコンソールに出力しています。

実行結果

Task 1 completed
Task 2 completed
Task 3 completed
1 2 3

実行結果を見ると、タスクが順番に完了していることがわかります。

このように、awaitを使うことで、非同期処理を同期的に実行できるのです。

○サンプルコード4:複数の非同期処理を並行して実行する

一方、複数の非同期処理を並行して実行したい場合は、Promise.allを使います。

次のコードは、3つの非同期関数を並行して実行する例です。

async function task1() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Task 1 completed');
      resolve(1);
    }, 1000);
  });
}

async function task2() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Task 2 completed');
      resolve(2);
    }, 500);
  });
}

async function task3() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Task 3 completed');
      resolve(3);
    }, 750);
  });
}

async function main() {
  const results = await Promise.all([task1(), task2(), task3()]);
  console.log(results);
}

main();

このコードでは、Promise.allに非同期関数の配列を渡しています。

Promise.allは、すべての非同期処理が完了するまで待機し、各処理の結果を配列で返します。

main関数の中では、await Promise.all([task1(), task2(), task3()])として、3つのタスクを並行して実行しています。

すべてのタスクが完了すると、結果の配列が返されます。

実行結果

Task 2 completed
Task 3 completed
Task 1 completed
[1, 2, 3]

実行結果を見ると、タスクが並行して実行されていることがわかります。

task2が最も早く完了し、次にtask3、最後にtask1が完了しています。

このように、Promise.allとawaitを組み合わせることで、複数の非同期処理を効率的に実行できます。

○サンプルコード5:非同期処理の結果を配列で受け取る

非同期処理の結果を配列で受け取りたい場合も、Promise.allを使います。

次のコードは、複数の非同期関数を実行し、その結果を配列で受け取る例です。

async function fetchData(id) {
  return new Promise(resolve => {
    setTimeout(() => {
      const data = { id, name: `User ${id}` };
      console.log(`Fetched data for User ${id}`);
      resolve(data);
    }, 1000);
  });
}

async function main() {
  const userIds = [1, 2, 3, 4, 5];
  const users = await Promise.all(userIds.map(id => fetchData(id)));
  console.log(users);
}

main();

このコードでは、fetchData関数が非同期的にデータを取得する処理をシミュレートしています。

main関数の中では、userIds配列に5人のユーザーIDが格納されています。

await Promise.all(userIds.map(id => fetchData(id)))として、各ユーザーIDに対してfetchData関数を呼び出し、その結果を配列で受け取っています。

mapを使うことで、userIds配列の各要素に対してfetchData関数を適用しています。

実行結果

Fetched data for User 1
Fetched data for User 2
Fetched data for User 3
Fetched data for User 4
Fetched data for User 5
[
  { id: 1, name: 'User 1' },
  { id: 2, name: 'User 2' },
  { id: 3, name: 'User 3' },
  { id: 4, name: 'User 4' },
  { id: 5, name: 'User 5' }
]

実行結果を見ると、各ユーザーのデータが取得され、最終的に配列としてまとめられていることがわかります。

このように、Promise.allとawaitを使うことで、複数の非同期処理の結果を配列で簡単に受け取ることができます。

○サンプルコード6:非同期処理のエラーをキャッチする

非同期処理でエラーが発生した場合、try...catch文を使ってエラーをキャッチすることができます。

次のコードは、非同期処理でエラーが発生した場合の処理例です。

async function fetchData(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 3) {
        reject(new Error('Failed to fetch data for User 3'));
      } else {
        const data = { id, name: `User ${id}` };
        console.log(`Fetched data for User ${id}`);
        resolve(data);
      }
    }, 1000);
  });
}

async function main() {
  const userIds = [1, 2, 3, 4, 5];

  try {
    const users = await Promise.all(userIds.map(id => fetchData(id)));
    console.log(users);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

main();

このコードでは、fetchData関数の中で、idが3の場合にエラーを発生させるようにしています。

reject関数を呼び出すことで、エラーを明示的に通知しています。

main関数の中では、try...catch文を使って非同期処理を実行しています。

エラーが発生した場合は、catchブロックでエラーをキャッチし、エラーメッセージをコンソールに出力します。

実行結果

Fetched data for User 1
Fetched data for User 2
Error: Failed to fetch data for User 3

実行結果を見ると、User 3のデータ取得でエラーが発生し、エラーメッセージが出力されていることがわかります。

このように、try...catch文を使うことで、非同期処理のエラーを適切にハンドリングできます。

○サンプルコード7:非同期処理のタイムアウトを設定する

非同期処理にタイムアウトを設定することで、一定時間経過しても処理が完了しない場合にエラーを発生させることができます。

次のコードは、非同期処理にタイムアウトを設定する例です。

async function fetchData(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { id, name: `User ${id}` };
      console.log(`Fetched data for User ${id}`);
      resolve(data);
    }, id * 1000);
  });
}

async function fetchDataWithTimeout(id, timeout) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`Timeout for User ${id}`));
    }, timeout);
  });

  return Promise.race([fetchData(id), timeoutPromise]);
}

async function main() {
  try {
    const user1 = await fetchDataWithTimeout(1, 500);
    console.log(user1);
  } catch (error) {
    console.error('Error:', error.message);
  }

  try {
    const user2 = await fetchDataWithTimeout(2, 3000);
    console.log(user2);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

main();

このコードでは、fetchDataWithTimeout関数を定義しています。

この関数は、fetchData関数とタイムアウト用のPromiseをPromise.raceで競合させます。

Promise.raceは、複数のPromiseのうち、最初に完了したPromiseの結果を返します。

main関数の中では、fetchDataWithTimeout関数を呼び出して、それぞれのユーザーデータの取得を試みています。

User 1の場合は、タイムアウト時間を500msに設定し、User 2の場合は、タイムアウト時間を3000msに設定しています。

実行結果

Error: Timeout for User 1
Fetched data for User 2
{ id: 2, name: 'User 2' }

実行結果を見ると、User 1のデータ取得ではタイムアウトが発生し、エラーメッセージが出力されています。

一方、User 2のデータ取得は成功しています。

このように、Promise.raceとawaitを使うことで、非同期処理にタイムアウトを設定し、一定時間経過しても処理が完了しない場合にエラーを発生させることができます。

●awaitを使う上でのよくあるエラーと対処法

awaitを使った非同期処理を極めるためには、エラーにうまく対処できるスキルも欠かせません。

初心者のうちは、awaitを使っていてよくわからないエラーに遭遇することがあるかもしれません。

でも、大丈夫です。エラーメッセージをしっかり読んで、原因を突き止められるようになりましょう。

ここでは、awaitを使う上でよく遭遇するエラーとその対処法を3つ紹介します。

エラーが発生した時は、落ち着いて原因を探ってみてください。

きっと、エラーを修正するためのヒントが見つかるはずです。

○エラー1:Uncaught SyntaxError: await is only valid in async function

Uncaught SyntaxError: await is only valid in async function

このエラーは、awaitキーワードがasync関数の外で使用されている場合に発生します。

awaitは、async関数の中でのみ有効です。

エラーの修正として、awaitを使用する関数にasyncキーワードを追加します。

// 誤った例
function fetchData() {
  const data = await fetch('https://api.example.com/data');
  return data.json();
}

// 修正後
async function fetchData() {
  const data = await fetch('https://api.example.com/data');
  return data.json();
}

asyncキーワードを関数に追加することで、その関数内でawaitを使用できるようになります。

async関数は、必ずPromiseを返すので、awaitを使って非同期処理の結果を待つことができます。

○エラー2:Uncaught TypeError: Cannot read property ‘then’ of undefined

Uncaught TypeError: Cannot read property 'then' of undefined

このエラーは、awaitの右辺にPromise以外の値が指定されている場合に発生します。

awaitは、Promiseの完了を待つためのキーワードなので、Promiseを返さない関数や値に対して使用することはできません。

エラーの修正として、awaitの右辺には、必ずPromiseを返す関数や式を指定します。

// 誤った例
async function fetchData() {
  const data = await 'Not a promise';
  return data;
}

// 修正後
async function fetchData() {
  const data = await Promise.resolve('This is a promise');
  return data;
}

awaitの右辺には、Promiseを返す関数や式を指定する必要があります。

例えば、fetch関数はPromiseを返すので、await fetch(...)のように使用できます。

Promiseを返さない値に対してawaitを使用すると、このようなエラーが発生します。

○エラー3:UnhandledPromiseRejectionWarning: Unhandled promise rejection

UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)

このエラーは、Promiseがリジェクトされた(失敗した)にもかかわらず、そのエラーがキャッチされていない場合に発生します。

Promiseのエラーは、.catch()メソッドを使ってハンドリングする必要があります。

エラーの修正として、Promiseを使用する際は、必ず.catch()メソッドを使ってエラーをキャッチするようにします。

// 誤った例
async function fetchData() {
  const data = await fetch('https://api.example.com/data');
  return data.json();
}

fetchData();

// 修正後
async function fetchData() {
  try {
    const data = await fetch('https://api.example.com/data');
    return data.json();
  } catch (error) {
    console.error('Error:', error);
  }
}

fetchData();

async関数内でawaitを使用する際は、try...catch文を使ってエラーをキャッチすることが重要です。

Promiseがリジェクトされた場合、そのエラーはcatchブロックで処理されます。

エラーをキャッチすることで、アプリケーションの予期しない動作を防ぐことができます。

●awaitのさらなる活用例

ここまで、awaitを使った非同期処理の基本から実践的な例まで学んできました。でも、awaitの活用はまだまだ終わりません。

awaitを使いこなすことで、もっと効率的で可読性の高いコードを書くことができるんです。

では、awaitのさらなる活用例を見ていきましょう。

ここでは、asyncイテレータやfor awaitループ、Promise.allを使った並列処理など、より発展的なawaitの使い方を紹介します。

また、Node.jsの最新バージョンで導入されたTop-Level awaitを使ったモジュールの初期化についても触れます。

これらの活用例を学ぶことで、awaitを使った非同期処理のスキルがさらに磨かれるはずです。

コードの可読性と効率を高めるために、ぜひ習得しておきたいテクニックばかりです。

それでは、一緒に学んでいきましょう。

○サンプルコード8:asyncイテレータとfor awaitループ

asyncイテレータとfor awaitループを使うと、非同期的に生成される値を順番に処理することができます。

次のコードは、asyncイテレータを使って非同期的に値を生成し、for awaitループで処理する例です。

async function* asyncGenerator() {
  yield new Promise(resolve => setTimeout(() => resolve(1), 1000));
  yield new Promise(resolve => setTimeout(() => resolve(2), 500));
  yield new Promise(resolve => setTimeout(() => resolve(3), 750));
}

async function main() {
  for await (const value of asyncGenerator()) {
    console.log(value);
  }
}

main();

このコードでは、asyncGenerator関数がasyncイテレータを返します。

asyncイテレータは、yieldキーワードを使って非同期的に値を生成します。

それぞれのyield文では、一定の時間(1000ms、500ms、750ms)が経過した後に値が解決されるPromiseを返しています。

main関数の中では、for await...ofループを使ってasyncイテレータから値を取得しています。

for await...ofループは、asyncイテレータが返す各Promiseが解決されるまで待機し、解決された値をvalue変数に代入します。

実行結果

1
2
3

実行結果を見ると、非同期的に生成された値が順番に出力されていることがわかります。

asyncイテレータとfor awaitループを使うことで、非同期処理を同期的に扱うことができるのです。

○サンプルコード9:awaitとPromise.allを使った並列処理

awaitとPromise.allを組み合わせることで、複数の非同期処理を並列に実行し、その結果を配列で受け取ることができます。

次のコードは、awaitとPromise.allを使って並列処理を行う例です。

async function task(id) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`Task ${id} completed`);
      resolve(id);
    }, Math.random() * 1000);
  });
}

async function main() {
  const taskIds = [1, 2, 3, 4, 5];
  const results = await Promise.all(taskIds.map(id => task(id)));
  console.log('Results:', results);
}

main();

このコードでは、task関数が非同期的なタスクをシミュレートしています。

それぞれのタスクは、ランダムな時間(0〜1000ms)が経過した後に完了します。

main関数の中では、taskIds配列に5つのタスクIDが格納されています。

Promise.alltaskIds.map(id => task(id))を渡すことで、各タスクIDに対してtask関数を呼び出し、それらを並列に実行しています。

await Promise.all(...)とすることで、すべてのタスクが完了するまで待機し、結果の配列をresults変数に代入します。

実行結果

Task 2 completed
Task 5 completed
Task 1 completed
Task 4 completed
Task 3 completed
Results: [1, 2, 3, 4, 5]

実行結果を見ると、タスクが並列に実行され、完了順にログが出力されていることがわかります。

最後に、すべてのタスクの結果が配列として取得されています。

awaitとPromise.allを使うことで、複数の非同期処理を効率的に実行できるのです。

○サンプルコード10:Top-Level awaitを使ったモジュールの初期化

Node.jsの最新バージョンでは、Top-Level awaitがサポートされています。

Top-Level awaitを使うと、モジュールのトップレベルでawaitを使用できるようになります。

次のコードは、Top-Level awaitを使ってモジュールを初期化する例です。

// utils.js
export async function initialize() {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('Initialization completed');
}

// main.js
import { initialize } from './utils.js';

await initialize();

console.log('Main module');

このコードでは、utils.jsモジュールにinitialize関数が定義されています。

initialize関数は、1秒間の遅延を持つPromiseを待機し、初期化が完了したことをログに出力します。

main.jsモジュールでは、utils.jsモジュールからinitialize関数をインポートしています。

そして、Top-Level awaitを使ってawait initialize()としています。

これにより、initialize関数の完了を待ってから、'Main module'がログに出力されます。

実行結果

Initialization completed
Main module

実行結果を見ると、initialize関数の完了を待ってから、main.jsモジュールの処理が続行されていることがわかります。

Top-Level awaitを使うことで、モジュールの初期化を同期的に扱うことができるのです。

まとめ

JavaScriptのawaitを使いこなすことで、非同期処理をより効率的に、可読性高く書けるようになります。

これからは、この記事で学んだことを活かして、自分のコードにawaitを積極的に取り入れてみてください。

複雑な非同期処理もawaitを使えばスッキリと書けるようになります。

コードの可読性が上がり、バグも減らせるでしょう。