Node.js Async/Await
1. Async/Await 入門
Async/Await は、Node.js における非同期操作(Asynchronous operations)を処理するためのモダンな手法です。Promise(プロミス) をベースに構築されており、さらに可読性の高いコードを作成することができます。
Node.js 7.6 で導入され、ES2017 で標準化された Async/Await を使用すると、非同期コードをあたかも同期コード(Synchronous code)のような見た目と挙動で記述できます。
本質的に Async/Await は、Promise をより読みやすい構文にしたものです。これによりコードはクリーンになり、メンテナンス性(Maintainability)が向上します。メインスレッド(Main thread)をブロックすることなく、追跡や理解が容易な非同期処理を実現します。
2. 構文と基本的な使い方
この構文は、2つのキーワードで構成されます。
async: Promise を返す非同期関数(Asynchronous function)を宣言するために使用します。await: Promise が解決(Resolve)されるまで実行を一時停止します。async関数の中でのみ使用可能です。
2.1 例:基本的な Async/Await
async function getData() {
console.log('開始...');
// 非同期操作が完了するまで待機
const result = await someAsyncOperation();
console.log(`結果: ${result}`);
return result;
}
function someAsyncOperation() {
return new Promise(resolve => {
// 1秒のディレイをシミュレート
setTimeout(() => resolve('操作完了'), 1000);
});
}
// 非同期関数を呼び出す
getData().then(data => console.log('最終データ:', data));2.2 例:Async/Await を使用したファイルの読み込み
const fs = require('fs').promises;
async function readFile() {
try {
// ファイルの読み込みを await
const data = await fs.readFile('myfile.txt', 'utf8');
console.log(data);
} catch (error) {
console.error('ファイル読み込みエラー:', error);
}
}
readFile();3. try/catch によるエラーハンドリング
Async/Await の大きな利点の一つは、従来通りの try/catch ブロックを使用してエラーハンドリング(Error handling)ができる点です。これにより、コードの可読性が大幅に向上します。
3.1 例:Async/Await でのエラーハンドリング
async function fetchUserData() {
try {
const response = await fetch('https://api.example.com/users/1');
if (!response.ok) {
// HTTPエラーをスロー
throw new Error(`HTTPエラー: ${response.status}`);
}
const user = await response.json();
console.log('ユーザーデータ:', user);
return user;
} catch (error) {
console.error('ユーザーデータ取得エラー:', error);
throw error; // 必要に応じてエラーを再スロー
}
}また、シナリオに応じて Async/Await と Promise の .catch() を組み合わせることも可能です。
// 非同期関数の外側で catch を使用する
fetchUserData().catch(error => {
console.log('非同期関数の外でキャッチされました:', error.message);
});4. Promise の並列実行
Async/Await はコードを同期的に見せますが、パフォーマンスを向上させるために複数の操作を並列(Parallel)で実行しなければならない場面もあります。
4.1 例:直列処理 vs 並列処理
// APIコールをシミュレートするヘルパー関数
function fetchData(id) {
return new Promise(resolve => {
setTimeout(() => resolve(`ID ${id} のデータ`), 1000);
});
}
// 直列処理 (Sequential) - 約3秒かかる
async function fetchSequential() {
console.time('sequential');
const data1 = await fetchData(1);
const data2 = await fetchData(2);
const data3 = await fetchData(3);
console.timeEnd('sequential');
return [data1, data2, data3];
}
// 並列処理 (Parallel) - 約1秒で完了
async function fetchParallel() {
console.time('parallel');
const results = await Promise.all([
fetchData(1),
fetchData(2),
fetchData(3)
]);
console.timeEnd('parallel');
return results;
}
// デモの実行
async function runDemo() {
console.log('直列処理を実行中...');
const seqResults = await fetchSequential();
console.log(seqResults);
console.log('\n並列処理を実行中...');
const parResults = await fetchParallel();
console.log(parResults);
}
runDemo();5. Async/Await vs Promises vs Callbacks
同じタスクを異なる非同期パターンでどのように処理するか比較してみましょう。
5.1 Callbacks(コールバック)の場合
function getUser(userId, callback) {
setTimeout(() => {
callback(null, { id: userId, name: 'John' });
}, 1000);
}
function getUserPosts(user, callback) {
setTimeout(() => {
callback(null, ['Post 1', 'Post 2']);
}, 1000);
}
// コールバックを使用
getUser(1, (error, user) => {
if (error) {
console.error(error);
return;
}
console.log('User:', user);
getUserPosts(user, (error, posts) => {
if (error) {
console.error(error);
return;
}
console.log('Posts:', posts);
});
});5.2 Promises(プロミス)の場合
function getUserPromise(userId) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ id: userId, name: 'John' });
}, 1000);
});
}
function getUserPostsPromise(user) {
return new Promise(resolve => {
setTimeout(() => {
resolve(['Post 1', 'Post 2']);
}, 1000);
});
}
// プロミスを使用
getUserPromise(1)
.then(user => {
console.log('User:', user);
return getUserPostsPromise(user);
})
.then(posts => {
console.log('Posts:', posts);
})
.catch(error => {
console.error(error);
});5.3 Async/Await の場合
// Async/Await を使用
async function getUserAndPosts() {
try {
const user = await getUserPromise(1);
console.log('User:', user);
const posts = await getUserPostsPromise(user);
console.log('Posts:', posts);
} catch (error) {
console.error(error);
}
}
getUserAndPosts();5.4 パターンの比較まとめ
| パターン | 長所 (Pros) | 短所 (Cons) |
|---|---|---|
| Callbacks | 理解が単純、広くサポートされている | コールバック地獄、エラーハンドリングが複雑 |
| Promises | .then() によるチェーン、エラーハンドリングの改善 | 複雑なフローではネストが発生、可読性は Async/Await に劣る |
| Async/Await | クリーンで同期的、try/catch が使いやすい | Promise の理解が必要、不用意に実行をブロックさせやすい |
6. ベストプラクティス
Node.js で Async/Await を扱う際は、以下のベストプラクティスに従ってください。
- Async 関数は常に Promise を返すことを忘れない
async function myFunction() {
return 'Hello';
}
// これは 'Hello' という文字列ではなく、'Hello' を解決する Promise を返す
const result = myFunction();
console.log(result); // Promise { 'Hello' }
// await するか .then() を使う必要がある
myFunction().then(message => console.log(message)); // Hello- 並列操作には
Promise.allを使用する - 操作が互いに独立している場合は、
Promise.allを使用してパフォーマンスを最適化(Optimization)しましょう。 - 常にエラーをハンドリングする
try/catchブロックを使用するか、非同期関数の呼び出しに.catch()をチェーンさせてください。- Async/Await とコールバックを混ぜない
- コールバックベースの関数は、
util.promisifyやカスタムラッパー(Wrapper)を使用して Promise ベースに変換してから使用しましょう。
const util = require('util');
const fs = require('fs');
// コールバックベースの関数を Promise ベースに変換
const readFile = util.promisify(fs.readFile);
async function readFileContents() {
const data = await readFile('file.txt', 'utf8');
return data;
}- 単一責任の原則を守る
- 非同期関数(Async functions)は、一つの責任(Responsibility)に集中するように設計しましょう。
トップレベル Await: Node.js 14.8.0 以降の ECMAScript モジュール(ESM)では、関数の外(モジュールレベル)で await を直接使用できる「トップレベル Await」機能が利用可能です。シングルスレッドの Node.js において、Async/Await はノンブロッキング(Non-blocking)な挙動を維持しつつ、コードの明快さを保つための強力な武器となります。