●awaitとは?非同期処理を極めるために知っておくべきこと
JavaScriptの非同期処理を極めるためには、awaitについて深く理解することが重要ですね。
awaitは非同期処理を同期的に書けるようにするための強力な機能ですが、使いこなすにはある程度の知識が必要です。
awaitを理解するためには、まずPromiseについて知っておく必要があります。
Promiseは非同期処理の結果を表すオブジェクトで、非同期処理の状態や結果値を保持します。
Promiseを使うことで、複雑な非同期処理をわかりやすく記述できるようになります。
○awaitを使う前に理解すべきPromiseの基本
Promiseは、次の3つの状態を持ちます。
- Pending(保留中) -> 非同期処理が完了していない状態
- Fulfilled(成功) -> 非同期処理が成功した状態
- Rejected(失敗) -> 非同期処理が失敗した状態
Promiseを使った非同期処理は、then
メソッドとcatch
メソッドを使って結果を処理します。
then
メソッドは、Promiseが成功した時に呼ばれ、catch
メソッドは失敗した時に呼ばれます。
○async/awaitの基本的な使い方
async/awaitは、Promiseをより簡潔に書くための構文です。
関数の前にasync
キーワードをつけることで、その関数は必ずPromiseを返すようになります。
また、await
キーワードを使うことで、Promiseの結果が返されるまで処理を待機できます。
awaitを使うには、次のようなルールがあります。
- awaitは、async関数の中でのみ使用できる
- awaitの右辺には、Promiseを返す式を置く
- 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();
このコードでは、task1
、task2
、task3
という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.all
にtaskIds.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を使えばスッキリと書けるようになります。
コードの可読性が上がり、バグも減らせるでしょう。