TypeScript 非同期処理 async/await 完全ガイド
はじめに
現代のソフトウェア開発において、非同期処理は不可欠な要素です。特にネットワーク通信、ファイルI/O、データベースアクセス、またはUIイベント処理といった、完了までに時間のかかる操作を行う際には、プログラムが応答不能にならないよう、メインスレッドをブロックせずにこれらの処理を進める必要があります。JavaScript、そしてそのスーパーセットであるTypeScriptにおいても、非同期処理は非常に重要な概念です。
歴史的に、JavaScriptの非同期処理はコールバック関数によって扱われてきました。しかし、複雑な非同期処理が連鎖すると、いわゆる「コールバック地獄 (Callback Hell)」と呼ばれる、コードの可読性やメンテナンス性を著しく低下させる問題が発生しました。この問題を解決するために登場したのがPromiseです。Promiseは非同期処理の最終的な結果(成功または失敗)を表すオブジェクトであり、非同期処理の連鎖やエラーハンドリングを構造化しやすくしました。
そして、Promiseの上に構築され、非同期処理のコードをまるで同期処理であるかのように、より直感的かつ簡潔に記述できるようになったのが、async
関数と await
演算子です。async/awaitはPromiseのシンタックスシュガー(糖衣構文)でありながら、その劇的な可読性の向上により、現代のJavaScript/TypeScript開発における非同期処理の主流となっています。
本記事では、TypeScriptにおける非同期処理の基本から始め、コールバック、Promiseといった進化の過程を振り返り、そしてasync/awaitの仕組み、使い方、詳細、ベストプラクティス、さらにはTypeScript特有の型に関する側面まで、async/awaitを「完全」に理解するための詳細なガイドを提供します。この記事を読むことで、非同期処理の概念をしっかりと把握し、async/awaitを自信を持ってTypeScriptプロジェクトで活用できるようになることを目指します。
さあ、非同期処理、そしてasync/awaitの世界へ深く潜っていきましょう。
非同期処理とは何か?
ソフトウェアの実行モデルには、大きく分けて「同期処理」と「非同期処理」があります。この違いを理解することは、非同期処理の必要性を把握する上で非常に重要です。
同期処理と非同期処理の違い
同期処理 (Synchronous Processing):
同期処理では、ある処理が完了するまで、次の処理は開始されません。タスクは順番に実行され、前のタスクが終わるのを待ってから次のタスクが始まります。これは非常に分かりやすいモデルですが、時間のかかる処理(例えば、ネットワークからのデータ取得)が含まれている場合、その処理が完了するまでプログラム全体が停止してしまいます。これを「ブロッキング (Blocking)」と呼びます。
例えば、以下の同期的なコードがあるとします。
typescript
console.log("処理1を開始");
// 時間のかかる処理(ここでは擬似的に表現)
const result = performSyncTask();
console.log("処理1が完了しました。結果:", result);
console.log("処理2を開始");
performAnotherSyncTask();
console.log("処理2が完了しました");
このコードでは、performSyncTask()
が終わるまで次の行の console.log
は実行されません。performSyncTask()
が1秒かかるとすれば、その間プログラムは「固まっている」状態になります。
非同期処理 (Asynchronous Processing):
非同期処理では、ある処理を開始したら、その完了を待たずにすぐに次の処理に移ります。時間のかかる処理はバックグラウンドで実行され、完了した時点で「通知」を受け取り、指定された後続の処理(コールバック関数など)を実行します。これは「ノンブロッキング (Non-blocking)」と呼ばれます。
非同期処理のイメージは、レストランで注文した料理を待っている間に、別の人が注文を聞きに来たり、水を注いだりするようなものです。最初の料理ができあがるのをただ待っているのではなく、他のタスクも同時に進められます。
JavaScriptはシングルスレッド言語であり、一度に一つの処理しか実行できません。しかし、非同期処理の仕組み(イベントループなど)を利用することで、時間のかかるI/O処理などをブラウザやNode.jsのバックグラウンドで行わせ、その間にもJavaScriptのメインスレッドで他の処理(UIの更新など)を実行できるようになっています。
なぜ非同期処理が必要なのか?
非同期処理は、特に以下のようなケースで不可欠です。
- I/O処理: ファイルの読み書き、データベースへのアクセスなどは、ストレージデバイスやネットワークの速度に依存し、CPUの計算に比べて非常に時間がかかります。これらの処理を同期的に行うと、プログラム全体がフリーズしてしまいます。
- ネットワーク通信: 外部APIへのリクエスト、サーバーからのデータ受信などは、通信速度やサーバーの応答時間に依存します。Webアプリケーションやモバイルアプリケーションで、ネットワーク通信中にUIが応答しなくなるのは許容できません。
- タイマー処理:
setTimeout
やsetInterval
のように、一定時間後に処理を実行したり、一定間隔で処理を繰り返したりする場合も非同期処理が利用されます。 - UI操作: ブラウザ環境では、ユーザーのクリックや入力といったイベントは非同期に発生します。これらのイベントに対する処理は、メインスレッドをブロックしないように非同期で行う必要があります。また、重い計算処理などでUIの更新がブロックされるのを避けるためにも、非同期または分割して処理を行うことがあります。
これらのケースで同期処理を選択すると、ユーザー体験を著しく損ねたり、システム全体のスループットを低下させたりする可能性があります。したがって、非同期処理は現代のレスポンシブで効率的なアプリケーション開発において必須の技術です。
JavaScript/TypeScriptにおける非同期処理の進化
JavaScript/TypeScriptにおける非同期処理の実現方法は、言語の進化とともに改善されてきました。主な段階として、コールバック、Promise、そしてasync/awaitがあります。
コールバック (Callbacks)
非同期処理の最も基本的なパターンは、コールバック関数を使用することです。非同期関数を呼び出す際に、その処理が完了した後に実行してほしい処理を関数として渡します。非同期関数は自身の処理を開始し、すぐに呼び出し元に戻ります。そして、非同期処理が完了した際に、渡されたコールバック関数を呼び出します。
例えば、擬似的なファイル読み込み関数を考えてみましょう。
``typescript
${filePath} の読み込みを開始…
// 擬似的な非同期ファイル読み込み関数
function readFileAsync(filePath: string, callback: (error: Error | null, data?: string) => void): void {
console.log();
これは ${filePath} の内容です。`;
// 実際のファイル読み込みは時間のかかる処理
setTimeout(() => {
const mockError = Math.random() > 0.8 ? new Error("読み込みエラーが発生しました") : null;
const mockData = mockError ? undefined :
if (mockError) {
console.error(`${filePath} の読み込みに失敗しました`);
callback(mockError); // エラーを渡してコールバック呼び出し
} else {
console.log(`${filePath} の読み込みが完了しました`);
callback(null, mockData); // データを渡してコールバック呼び出し
}
}, 1000); // 1秒後に完了すると仮定
}
// 使用例
console.log(“メイン処理開始”);
readFileAsync(“file1.txt”, (error, data) => {
if (error) {
console.error(“ファイル1の処理でエラー:”, error.message);
} else {
console.log(“ファイル1の内容:”, data);
// ファイル1の読み込みが成功したら、次にファイル2を読み込む
readFileAsync("file2.txt", (error2, data2) => {
if (error2) {
console.error("ファイル2の処理でエラー:", error2.message);
} else {
console.log("ファイル2の内容:", data2);
// ファイル2の読み込みが成功したら、次にファイル3を読み込む...
readFileAsync("file3.txt", (error3, data3) => {
if (error3) {
console.error("ファイル3の処理でエラー:", error3.message);
} else {
console.log("ファイル3の内容:", data3);
console.log("全てのファイル処理が完了しました");
}
});
}
});
}
});
console.log(“メイン処理終了 (非同期処理はバックグラウンドで続行)”);
“`
この例のように、コールバックを使用すると、処理が完了した順序で後続の処理を実行できます。
コールバックのメリット:
* 非同期処理を実現する最も原始的でシンプルな方法。
* 小さな非同期処理には十分。
コールバックのデメリット (Callback Hell):
* コードのネストが深くなる: 上の例のように、複数の非同期処理を順次実行しようとすると、コールバック関数の中にさらにコールバック関数を書くことになり、コードのインデントがどんどん深くなります。これが「Callback Hell」と呼ばれる状態です。ネストが深くなると、コードの流れを追うのが非常に難しくなり、可読性が著しく低下します。
* エラーハンドリングが煩雑: 各非同期処理のコールバック内でエラーをチェックし、適切に処理する必要があります。エラーが連鎖する場合、エラー処理のロジックもネストしてしまい、複雑になります。
* 制御フローの管理が困難: 並列処理や、特定の条件に基づいたスキップなど、複雑な制御フローを実現するのが難しくなります。
* 複数のコールバック引数: コールバック関数は通常、最初のエラー引数とそれに続く結果引数を受け取ります(Node.jsスタイルのコールバック)。引数の順序や意味を常に把握しておく必要があります。
コールバック地獄は、大規模な非同期処理を含むアプリケーション開発において深刻な問題でした。この問題を解決するために、Promiseが登場しました。
Promise (プロミス)
Promiseは、非同期操作の完了(または失敗)とその結果の値を表現するオブジェクトです。Promiseは、非同期処理の「未来の値」に対するプレースホルダーと考えることができます。Promiseを使うことで、非同期処理をより構造化された方法で記述できるようになります。
Promiseには以下の3つの状態があります。
1. Pending (保留): 非同期操作がまだ完了していない初期状態。
2. Fulfilled (成功): 非同期操作が成功裏に完了し、結果の値が確定した状態。
3. Rejected (失敗): 非同期操作が失敗し、エラー理由が確定した状態。
Promiseは一度状態がPendingからFulfilledまたはRejectedに遷移すると、その状態は二度と変化しません。この遷移を「settled (決着済み)」と呼びます。
new Promise()
の使い方
Promiseベースの非同期関数を作成するには、new Promise()
コンストラクタを使用します。コンストラクタは1つの引数、executor 関数を受け取ります。executor関数は、resolve
と reject
という2つの引数を取る関数です。
* resolve(value)
: 非同期処理が成功した場合に呼び出し、Promiseの状態をFulfilledにし、value
を結果として渡します。
* reject(reason)
: 非同期処理が失敗した場合に呼び出し、Promiseの状態をRejectedにし、reason
(通常はErrorオブジェクト) を理由として渡します。
``typescript
${filePath} の読み込みを開始… (Promise)
// 擬似的なPromiseベースの非同期関数
function readFilePromise(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
console.log();
これは ${filePath} の内容です。 (Promise)`;
setTimeout(() => {
const mockError = Math.random() > 0.8 ? new Error("読み込みエラーが発生しました (Promise)") : null;
const mockData = mockError ? undefined :
if (mockError) {
console.error(`${filePath} の読み込みに失敗しました (Promise)`);
reject(mockError); // 失敗時は reject を呼び出す
} else {
console.log(`${filePath} の読み込みが完了しました (Promise)`);
resolve(mockData as string); // 成功時は resolve を呼び出す
}
}, 1000);
});
}
“`
.then()
, .catch()
, .finally()
Promiseオブジェクトには、非同期処理の完了後に実行される処理を登録するためのメソッドが用意されています。
.then(onFulfilled, onRejected)
: PromiseがFulfilledになったときに実行されるonFulfilled
関数、またはRejectedになったときに実行されるonRejected
関数を登録します。onRejected
は省略可能です。.then()
は新しいPromiseを返すため、Promiseチェーンを構築できます。.catch(onRejected)
: PromiseがRejectedになったときに実行されるonRejected
関数を登録します。これは.then(undefined, onRejected)
と同等ですが、エラーハンドリングに特化しているため、より可読性が高まります。.catch()
も新しいPromiseを返します。.finally(onSettled)
: PromiseがFulfilledまたはRejectedのどちらかの状態になったときに、必ず実行されるonSettled
関数を登録します。.finally()
も新しいPromiseを返します。クリーンアップ処理などに便利です。
これらのメソッドを使うと、コールバック地獄を回避し、非同期処理の連鎖をよりフラットに記述できます。
“`typescript
// Promiseの使用例(チェーン)
console.log(“メイン処理開始 (Promise)”);
readFilePromise(“file1.txt”)
.then(data1 => {
console.log(“ファイル1の内容:”, data1);
// ファイル1の読み込みが成功したら、次にファイル2を読み込むPromiseを返す
return readFilePromise(“file2.txt”);
})
.then(data2 => {
console.log(“ファイル2の内容:”, data2);
// ファイル2の読み込みが成功したら、次にファイル3を読み込むPromiseを返す
return readFilePromise(“file3.txt”);
})
.then(data3 => {
console.log(“ファイル3の内容:”, data3);
console.log(“全てのファイル処理が完了しました (Promise)”);
})
.catch(error => {
// チェーン内のいずれかのPromiseがRejectedになった場合、ここに到達する
console.error(“Promiseチェーンでエラーが発生しました:”, error.message);
})
.finally(() => {
console.log(“Promiseチェーンの処理が終了しました (fulfilled or rejected)”);
});
console.log(“メイン処理終了 (Promise 非同期処理はバックグラウンドで続行)”);
“`
このコードは、コールバックの例よりもずっと読みやすくなりました。非同期処理の流れが .then()
で順に記述され、エラー処理も .catch()
で一箇所に集約できます。
複数のPromiseを扱うメソッド
Promiseクラスには、複数のPromiseをまとめて扱うための便利な静的メソッドがいくつかあります。
Promise.all(iterable)
: 与えられたPromiseオブジェクトの配列(iterable)が全てFulfilledになった場合に、その結果を配列として持つ新しいPromiseを返します。一つでもRejectedになった場合、その時点でRejectedとなり、他のPromiseの結果は無視されます。並列処理によく使われます。Promise.race(iterable)
: 与えられたPromiseオブジェクトの配列のうち、どれか一つが先にFulfilledまたはRejectedになった場合に、その結果(または理由)を持つ新しいPromiseを返します。Promise.any(iterable)
: 与えられたPromiseオブジェクトの配列のうち、どれか一つが先にFulfilledになった場合に、その結果を持つ新しいPromiseを返します。全てのPromiseがRejectedになった場合にのみ、AggregateError
を理由としてRejectedになります。Promise.allSettled(iterable)
: 与えられたPromiseオブジェクトの配列が全てSettled (FulfilledまたはRejected) になった場合に、それぞれのPromiseの状態と結果/理由を含むオブジェクトの配列を持つ新しいPromiseを返します。これは、個々のPromiseの成功・失敗に関わらず、全ての結果を知りたい場合に便利です。
“`typescript
// Promise.all の使用例(並列処理)
console.log(“並列ファイル読み込み開始 (Promise.all)”);
Promise.all([
readFilePromise(“parallel_file1.txt”),
readFilePromise(“parallel_file2.txt”),
readFilePromise(“parallel_file3.txt”)
])
.then(results => {
console.log(“全ての並列ファイル読み込みが成功しました (Promise.all)”);
results.forEach((data, index) => {
console.log(並列ファイル${index + 1}の内容:
, data);
});
})
.catch(error => {
console.error(“並列ファイル読み込み中にエラーが発生しました (Promise.all):”, error.message);
});
“`
Promiseのメリットとデメリット
Promiseのメリット:
* 非同期処理の構造化: コールバック地獄を解消し、非同期処理の連鎖を.then()
で記述できる。
* エラーハンドリングの改善: .catch()
でエラーを一箇所に集約できる。
* 複数の非同期処理の管理: Promise.all
などの便利なメソッドが提供されている。
* 状態管理: Pending, Fulfilled, Rejectedという明確な状態を持つため、非同期処理の現在の状況を把握しやすい。
Promiseのデメリット:
* まだ完全には同期処理のようには見えない: .then()
や.catch()
を使う構文は、同期処理の try...catch
や順次実行とは異なるため、非同期処理のコードを読み解くにはPromiseの仕組みを理解している必要がある。特に、非同期処理の連鎖が長くなると、.then()
のネストが発生したり、コードが横に長くなったりして、Promiseチェーンでも可読性の限界を感じることがある。
* エラーが掴みにくい場合がある: .catch()
を付け忘れると、RejectedされたPromiseのエラーが捕捉されずに、未処理のPromise rejectionエラーとして報告されることがある。
* async/awaitに比べて冗長: async/awaitと比較すると、同じ処理を記述する際に.then()
, .catch()
を使う分、コード量が多くなる傾向がある。
Promiseは非同期処理の記述を大幅に改善しましたが、それでもまだ同期処理のような直感的な書き方とは少し隔たりがありました。ここでasync/awaitが登場します。
async/awaitの基本
async/awaitは、Promiseに基づいた非同期処理の記述を、より同期処理に近い構文で書けるようにする機能です。これはES2017で導入され、Promiseの使いやすさをさらに向上させました。TypeScriptでもバージョン2.1からサポートされています。
async/awaitを使用する上で核となるのは、以下の2つのキーワードです。
async
関数await
演算子
async関数の定義 (async function
)
async
キーワードは、関数の宣言の前に付けます。async
関数は、常にPromiseを返します。
“`typescript
async function myAsyncFunction(): Promise
// … 非同期処理 …
return “完了しました”; // string型を返しても、自動的に Promise
}
// またはアロー関数
const anotherAsyncFunction = async (): Promise
// … 非同期処理 …
return 123; // number型を返しても、自動的に Promise
};
“`
async
関数内で値を return
すると、その値は解決済みのPromise (Fulfilled) の結果としてラップされます。
例えば、async function greeting() { return "Hello"; }
は Promise.resolve("Hello")
を返すのと同等です。
async
関数内で例外を throw
すると、その関数はRejectedされたPromiseを返します。
例えば、async function failFunction() { throw new Error("Something went wrong"); }
は Promise.reject(new Error("Something went wrong"))
を返すのと同等です。
async
関数がPromiseを返すことは非常に重要です。これにより、async
関数は他のPromiseベースのAPIとシームレスに連携できます。async
関数の呼び出し元は、返されたPromiseに対して .then()
, .catch()
を使用することができます。
typescript
myAsyncFunction()
.then(result => {
console.log("async関数の結果:", result); // "完了しました"
})
.catch(error => {
console.error("async関数でエラー:", error);
});
await演算子 (await
)
await
キーワードは、Promiseの前に付けます。await
は、右辺のPromiseがSettled (FulfilledまたはRejected) になるまで、async関数の実行を一時停止します。
- PromiseがFulfilledされた場合、
await
式はPromiseが解決された値を返します。 - PromiseがRejectedされた場合、
await
式はPromiseが拒否された理由(通常はErrorオブジェクト)を投げます。この例外は、async関数内でtry...catch
ブロックを使用して捕捉できます。
重要な制約: await
演算子は、async
関数内でのみ使用できます。async関数ではない普通の関数や、スクリプトのトップレベル(モジュール環境を除く)で await
を単独で使用することはできません。
“`typescript
// 前述の Promise ベース関数
function delay(ms: number): Promise
return new Promise(resolve => setTimeout(resolve, ms));
}
async function demonstrateAwait() {
console.log(“処理開始”);
// await は delay(1000) が完了するまでここで一時停止する
await delay(1000);
console.log("1秒経過");
await delay(1000);
console.log("さらに1秒経過");
const result = await readFilePromise("data.txt"); // readFilePromise が完了するまで一時停止
console.log("ファイル読み込み完了:", result);
console.log("処理終了");
}
console.log(“async関数を呼び出し”);
demonstrateAwait(); // async関数はPromiseを返すので、呼び出しは即座に完了し、後続の処理は非同期に行われる
console.log(“async関数呼び出し後の処理”);
“`
この例では、await delay(1000);
の行で demonstrateAwait
関数の実行が一時停止します。しかし、これはJavaScriptのメインスレッド全体をブロックするわけではありません。async関数は内部的にPromiseとジェネレーターを使って実現されており、await
の場所で関数の実行を「中断」し、Promiseが解決または拒否されたときに実行を「再開」します。この間、JavaScriptのイベントループは他のタスク(UIイベントの処理や他の非同期処理など)を実行できます。
await
の右辺は必ずしもPromiseである必要はありません。非Promise値(文字列、数値、オブジェクトなど)を await
した場合、その値はすぐに返されます。これは同期的な return
とほぼ同じ挙動ですが、厳密には値を解決したPromiseを返す Promise.resolve(value) と同等であり、非同期タスクキューに一度入ってから実行されるため、同期的な処理とは少しだけ実行順序が異なる場合があります。しかし、通常の使用においては、非Promise値をawaitしても問題ありません。
“`typescript
async function awaitNonPromise() {
console.log(“非Promise await 前”);
const value = await 123; // これはすぐに解決される
console.log(“非Promise await 後:”, value);
await delay(0); // 念のためタスクキューの順番を確認
console.log(“delay(0) 後”);
}
console.log(“awaitNonPromise 呼び出し前”);
awaitNonPromise();
console.log(“awaitNonPromise 呼び出し後”);
// 実行順序の可能性 (環境による):
// awaitNonPromise 呼び出し前
// 非Promise await 前
// awaitNonPromise 呼び出し後
// 非Promise await 後: 123
// delay(0) 後
“`
基本的なasync/awaitの使用例
Promiseチェーンで書いたファイル読み込みの例を、async/awaitで書き直してみましょう。
``typescript
${filePath} の読み込みを開始…
// 前述の Promise ベース関数
function readFilePromise(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
console.log();
読み込みエラーが発生しました (${filePath})
setTimeout(() => {
const mockError = Math.random() > 0.8 ? new Error() : null;
これは ${filePath} の内容です。`;
const mockData = mockError ? undefined :
if (mockError) {
console.error(`${filePath} の読み込みに失敗しました`);
reject(mockError);
} else {
console.log(`${filePath} の読み込みが完了しました`);
resolve(mockData as string);
}
}, 1000);
});
}
async function processFilesSequentially() {
console.log(“ファイル処理開始 (async/await)”);
try {
// await で Promise の完了を待つ
const data1 = await readFilePromise(“fileA.txt”);
console.log(“ファイルAの内容:”, data1);
const data2 = await readFilePromise("fileB.txt");
console.log("ファイルBの内容:", data2);
const data3 = await readFilePromise("fileC.txt");
console.log("ファイルCの内容:", data3);
console.log("全てのファイル処理が完了しました (async/await)");
} catch (error: any) { // TypeScriptではエラーの型アノテーションが必要な場合がある
console.error("async/awaitでエラーが発生しました:", error.message);
}
}
console.log(“processFilesSequentially 呼び出し前”);
processFilesSequentially();
console.log(“processFilesSequentially 呼び出し後”);
“`
このコードは、Callback HellやPromiseチェーンの例と比較して、非常に読みやすく、同期処理のコードとほとんど見分けがつきません。非同期処理の流れが上から下へと自然に記述されています。エラーハンドリングも try...catch
ブロックという、同期処理でお馴染みの構文で行えるため、直感的です。
async/awaitは、非同期処理の可読性と保守性を劇的に向上させる強力なツールです。しかし、その能力を最大限に引き出し、潜在的な落とし穴を避けるためには、さらに詳細な理解が必要です。
async/awaitの詳細
async/awaitを使いこなすために、さらにいくつかの側面を見ていきましょう。
エラーハンドリング (Error Handling)
async関数内で発生したエラーや、awaitしたPromiseがRejectedされた場合のエラーは、同期処理と同様に try...catch
ブロックで捕捉できます。
“`typescript
async function fetchData(url: string): Promise
const response = await fetch(url); // fetchはPromiseを返す
if (!response.ok) {
// HTTPエラーの場合
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.text(); // response.text()もPromiseを返す
return data;
}
async function loadAndProcessData(url: string) {
try {
const data = await fetchData(url); // fetchDataがRejectedされる可能性あり
console.log(“データ取得成功:”, data);
// 取得したデータを使った後続処理…
} catch (error: any) {
// fetchData内で throw されたエラー、または fetch/response.text() がRejectedされた場合に捕捉される
console.error(“データ処理中にエラーが発生しました:”, error.message);
} finally {
console.log(“データ処理試行終了”);
}
}
loadAndProcessData(“https://example.com/some-data”);
loadAndProcessData(“https://example.com/non-existent-data”);
“`
await somePromise
は、somePromise
がRejectedされると例外をスローします。この例外は、await式を囲む try...catch
ブロックによって捕捉できます。
また、async関数自体からPromiseがRejectedされるのは、そのasync関数内で捕捉されなかった例外がスローされた場合です。
“`typescript
async function mightFail() {
console.log(“処理中…”);
if (Math.random() > 0.5) {
throw new Error(“意図的なエラー”); // このエラーは async 関数から Rejected な Promise として返される
}
return “成功!”;
}
async function callMightFail() {
try {
const result = await mightFail(); // mightFail() が Rejected されるとここで例外が発生
console.log(“mightFail 成功:”, result);
} catch (error: any) {
console.error(“callMightFail 内でエラー捕捉:”, error.message);
}
}
callMightFail();
callMightFail(); // 別の実行でエラーが発生する可能性もある
“`
Promiseチェーンの .catch()
は、チェーン内の任意の場所で発生したエラーをまとめて捕捉できるという利点がありました。async/awaitの try...catch
も同様に、try
ブロック内の任意の await
式または同期的なコードで発生した例外を捕捉できます。
複数の非同期処理の扱い
複数の非同期処理を行う場合、直列に実行するか、並列に実行するかを選択できます。
直列処理 (Sequential Execution)
複数の await
式を連続して記述すると、それらの非同期処理は順番に実行されます。前の await
が完了するまで次の await
は実行されません。これは、例えばファイルの読み込みやネットワークリクエストが互いに依存している場合(前のリクエストの結果を使って次のリクエストのURLを決めるなど)に適しています。
“`typescript
async function processSequentially() {
console.log(“直列処理開始”);
const data1 = await readFilePromise(“file1.txt”);
console.log(“file1.txt 処理完了”);
const data2 = await readFilePromise(“file2.txt”);
console.log(“file2.txt 処理完了”);
const data3 = await readFilePromise(“file3.txt”);
console.log(“file3.txt 処理完了”);
console.log(“直列処理終了”);
}
processSequentially();
“`
このコードでは、file1.txt
の読み込みが完了してから file2.txt
の読み込みが開始され、同様に file2.txt
の完了を待ってから file3.txt
が開始されます。全体の実行時間は、個々の処理時間の合計になります。
並列処理 (Parallel Execution)
非同期処理が互いに依存しない場合、それらを並列に実行することで全体の実行時間を短縮できます。async/awaitで並列処理を実現するには、Promise.all()
と await
を組み合わせます。まず、実行したい非同期関数(Promiseを返すもの)を全て呼び出し、それらを Promise.all()
に渡します。そして、その Promise.all()
が返すPromiseを await
します。
“`typescript
async function processInParallel() {
console.log(“並列処理開始”);
// 複数のPromiseを同時に開始させる
const promise1 = readFilePromise(“fileA.txt”);
const promise2 = readFilePromise(“fileB.txt”);
const promise3 = readFilePromise(“fileC.txt”);
// Promise.all が全てのPromiseが解決されるまで待つ
try {
const results = await Promise.all([promise1, promise2, promise3]);
console.log("全ての並列処理が完了しました");
console.log("結果1:", results[0]);
console.log("結果2:", results[1]);
console.log("結果3:", results[2]);
} catch (error: any) {
console.error("並列処理中にエラーが発生しました:", error.message);
}
console.log("並列処理終了");
}
processInParallel();
“`
このコードでは、readFilePromise
の呼び出しは await
されないため、それぞれのPromiseはほぼ同時に開始されます。await Promise.all(...)
は、これら3つのPromiseが全て完了するのを待ちます。全体の実行時間は、最も時間のかかった非同期処理の時間にほぼ等しくなります。
並列処理はパフォーマンス向上に非常に有効ですが、エラーハンドリングには注意が必要です。Promise.all()
に渡されたPromiseのうち一つでもRejectedされると、Promise.all()
はその最初のエラーで即座にRejectedされます。他のPromiseがどうなったか(完了したか、まだ実行中か)はデフォルトでは分かりません。全ての結果(成功・失敗に関わらず)を知りたい場合は、Promise.allSettled()
を使用します。
競合・最初の完了 (Promise.race()
, Promise.any()
)
複数の非同期処理を開始し、その中で最初に完了したPromiseの結果だけが必要な場合は Promise.race()
を使用します。
“`typescript
async function racePromises() {
console.log(“競合処理開始”);
const fastPromise = delay(500).then(() => “Fast result”);
const slowPromise = delay(1500).then(() => “Slow result”);
try {
// どちらか早い方が解決または拒否されるまで待つ
const result = await Promise.race([fastPromise, slowPromise]);
console.log("最初に完了した結果:", result); // "Fast result" が出力される可能性が高い
} catch (error: any) {
console.error("競合中にエラー発生:", error.message);
}
console.log("競合処理終了");
}
racePromises();
“`
Promise.race()
は、競合するPromiseのうち、最初にsettledしたPromiseの結果(または理由)を返します。
一方、複数のPromiseを開始し、最初にFulfilledになったPromiseの結果だけが必要で、全てがRejectedになった場合にのみエラーとしたい場合は Promise.any()
を使用します。
“`typescript
async function anyPromise() {
console.log(“どれか一つ成功処理開始”);
const failPromise1 = delay(500).then(() => { throw new Error(“Fail 1”); });
const failPromise2 = delay(1000).then(() => { throw new Error(“Fail 2”); });
const successPromise = delay(1500).then(() => “Success!”);
try {
// 最初の成功を待つ。全て失敗したらエラー
const result = await Promise.any([failPromise1, failPromise2, successPromise]);
console.log("最初の成功結果:", result); // "Success!" が出力される
} catch (error: any) {
// AggregateError が捕捉される
console.error("全てのPromiseが失敗しました:", error.message);
if (error instanceof AggregateError) {
error.errors.forEach((e: any) => console.error(" 個別のエラー:", e.message));
}
}
console.log("どれか一つ成功処理終了");
}
anyPromise();
“`
Promise.any()
は、少なくとも一つ成功すればOKというシナリオ(例えば複数のミラーサーバーから最速でデータを取得するなど)に適しています。全て失敗した場合は AggregateError
という特殊なエラーをスローします。
非同期イテレーターと for await...of
(Asynchronous Iterators and for await...of
)
非同期操作によって要素が生成されるストリームのようなデータソースを扱う場合、各要素を非同期的に取得して処理したいことがあります。ES2018で導入された非同期イテレーターと for await...of
ループは、このようなシナリオを同期的なループに近い構文で記述できるようにします。
非同期イテレーターは、[Symbol.asyncIterator]()
メソッドを持ち、このメソッドは非同期イテレーターオブジェクトを返します。非同期イテレーターオブジェクトは、next()
メソッドを持ち、このメソッドは { value: any, done: boolean }
の形をしたPromiseを返します。
for await...of
ループは、非同期イテラブルオブジェクト([Symbol.asyncIterator]()
を持つオブジェクト)または非同期イテレーターオブジェクトを反復処理するために使用されます。ループは非同期的に next()
メソッドを呼び出し、返されるPromiseが解決されるまで待機します。
“`typescript
// 擬似的な非同期データソース
async function* asyncGenerator() {
console.log(“ジェネレーター開始”);
await delay(1000);
yield 1;
console.log(“要素1生成”);
await delay(1000);
yield 2;
console.log(“要素2生成”);
await delay(1000);
yield 3;
console.log(“要素3生成”);
}
async function processAsyncIterator() {
console.log(“非同期イテレーション開始”);
// asyncGenerator() は非同期イテラブルを返す
for await (const value of asyncGenerator()) {
console.log(“イテレーターから値を取得:”, value);
// 各要素の処理も非同期で行われる可能性
await delay(500);
}
console.log(“非同期イテレーション終了”);
}
processAsyncIterator();
“`
async function*
構文は、非同期ジェネレーター関数を定義します。非同期ジェネレーター関数は、yield
演算子を使用して非同期的に値を生成できます。各 yield
の後に await
することで、要素の生成や処理の間で非同期的な待機を行うことができます。
for await...of
は、データベースカーソルのイテレーション、ファイルの行ごとの非同期読み込み、ネットワークストリームの処理など、要素が非同期的に利用可能になるコレクションやストリームの処理に非常に強力なツールです。
トップレベルawait (Top-level Await)
従来、await
はasync関数内でのみ使用可能でした。しかし、ES2022で導入され、多くの環境(Node.jsのESモジュール、最新のブラウザ環境でのESモジュール)でサポートされるようになった「トップレベルawait」により、モジュールのトップレベルで await
を使用できるようになりました。
“`typescript
// example.mjs または example.ts (tsc –module es2022 またはそれ以降)
// これは ES Module 環境でのみ有効です
import { readFilePromise } from ‘./utils’; // Promiseを返す関数と仮定
console.log(“モジュール処理開始”);
try {
const data = await readFilePromise(“config.json”);
const config = JSON.parse(data);
console.log(“設定ファイル読み込み完了:”, config);
// 設定を使った後続処理
const result = await fetchData(config.apiUrl);
console.log("APIデータ取得完了");
} catch (error: any) {
console.error(“初期化中にエラー:”, error.message);
// エラーが発生したら、モジュールのエクスポートなどをスキップするなど
// process.exit(1) などで終了することも可能(Node.jsの場合)
}
console.log(“モジュール処理終了 (await完了後)”);
// このモジュールが他のモジュールにインポートされた場合、
// インポート元のモジュールは、このモジュールのトップレベルawaitが完了するまで待機する。
export const initialized = true;
“`
トップレベルawaitは、モジュールの初期化処理で非同期操作が必要な場合に便利です。例えば、データベース接続の確立、設定ファイルの読み込み、外部APIキーの検証などをモジュールのロード時に非同期に行えます。
注意点: トップレベルawaitは、そのモジュールをインポートする他のモジュールのロードをブロックします。したがって、トップレベルawaitを多用しすぎると、アプリケーションの起動時間が遅くなる可能性があります。また、Scriptタイプのスクリプト(HTMLの <script>
タグで type="module"
がない場合や、Node.jsのCommonJSモジュール)では使用できません。
TypeScriptにおけるasync/await
TypeScriptはJavaScriptのスーパーセットであるため、JavaScriptでasync/awaitが使える環境であれば、TypeScriptでも同様に使えます。TypeScriptはasync/await構文を完全にサポートしており、さらに型安全性の恩恵を受けられます。
型定義 (Type Definitions)
TypeScriptでは、async関数やawaitの結果に対する型が自動的に推論されます。
- async関数の戻り値の型: 前述のように、
async function
は常にPromiseを返します。async関数内でreturn value;
とした場合、その関数の戻り値型はPromise<TypeOfValue>
と推論されます。明示的に型注釈を付けることもできます。
“`typescript
// 戻り値型が Promise
async function fetchGreeting() {
const response = await fetch(“/greeting”);
const text = await response.text();
return text; // string を返しているが、async なので Promise
}
// 明示的に型注釈を付ける
async function fetchUser(id: number): Promise<{ id: number; name: string }> {
const response = await fetch(/users/${id}
);
const user = await response.json();
return user; // user オブジェクトを返しているが、async なので Promise<{ id: number; name: string }> になる
}
“`
await
で得られる値の型:await
したPromiseの型がPromise<T>
である場合、await
式の結果の型はT
になります。つまり、Promiseが解決された際に得られる値の型がそのままawaitの結果の型になります。
“`typescript
async function processData() {
const greetingPromise: Promise
const greeting: string = await greetingPromise; // await の結果は string 型
const userPromise: Promise<{ id: number; name: string }> = fetchUser(1);
const user: { id: number; name: string } = await userPromise; // await の結果は { id: number; name: string } 型
console.log(greeting.toUpperCase()); // string 型として安全に扱える
console.log(user.name); // user オブジェクトとして安全に扱える
}
“`
TypeScriptの型推論は強力ですが、複雑な非同期処理や外部ライブラリを使用する際には、明示的な型注釈がコードの可読性やメンテナンス性を向上させることがあります。特に、非同期関数やPromiseを返す関数の戻り値型には、Promise<T>
の形で適切な型を付けておくと良いでしょう。
ジェネリック型とasync/await (Promise<T>
)
Promiseはジェネリック型 (Promise<T>
) を使用して、非同期処理が成功した場合にどのような型の値を返すかを表現します。async関数も同様に、戻り値型として Promise<T>
を使用します。
``typescript
HTTP error! status: ${response.status}`);
// T型の値を返すPromiseを返す async 関数
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
}
const data: T = await response.json(); // ここで T 型としてアサートまたは推論させる
return data;
}
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
name: string;
price: number;
}
async function exampleUsage() {
try {
// fetchData
// await の結果は User 型になる
const user: User = await fetchData
console.log(“ユーザーデータ:”, user.name, user.email);
// fetchData<Product> を呼び出すと、戻り値は Promise<Product> になり、
// await の結果は Product 型になる
const product: Product = await fetchData<Product>("/api/product/abc");
console.log("製品データ:", product.name, product.price);
} catch (error: any) {
console.error("エラー:", error.message);
}
}
“`
ジェネリック型をasync/awaitと組み合わせることで、様々な型の非同期操作を型安全に扱う共通関数を作成できます。
tsconfigの設定 (target
, module
)
async/await構文を使用するには、TypeScriptコンパイラ (tsc
) が適切なJavaScriptバージョンに出力できるよう、tsconfig.json
で以下のオプションを設定する必要があります。
"target"
: async/awaitがサポートされているJavaScriptバージョン(例:"es2017"
,"es2018"
,"esnext"
)以上を指定します。古いバージョン(例:"es5"
,"es2015"
)を指定した場合、TypeScriptコンパイラはasync/awaitをPromiseを使ったコードに「ダウンパイル」します。ダウンパイルにはランタイム環境でのPromiseとGeneratorのサポートが必要ですが、モダンな環境では問題ありません。"module"
: 使用するモジュールシステム(例:"commonjs"
,"es2015"
,"esnext"
)を指定します。トップレベルawaitを使用する場合は、"module"
を"es2022"
または"esnext"
に設定する必要があります。
例:
json
{
"compilerOptions": {
"target": "es2017", // または "esnext"
"module": "commonjs", // または "esnext", "es2022" など
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": [
"src/**/*.ts"
]
}
async/awaitを古いJavaScript環境で実行する必要がある場合は、適切なtarget
設定に加えて、PromiseやGeneratorのポリフィル(polyfill)が必要になる場合があります。
async/awaitのベストプラクティスと注意点
async/awaitは非同期処理を容易にしますが、効果的に安全に使用するためにはいくつかのベストプラクティスと注意点があります。
Promiseを待つことの重要性:awaitし忘れることによる問題
async関数内でPromiseを返す関数を呼び出した際に await
を付け忘れると、そのPromiseの完了を待たずに次の行が実行されてしまいます。これは意図しない並列処理や、Promiseが完了する前にその結果にアクセスしようとしてエラーが発生する原因となります。
“`typescript
async function badExample() {
console.log(“Bad example start”);
const promise = delay(1000); // await し忘れている!
console.log(“この行は delay が完了する前に実行される”); // 問題その1
// promise がまだ完了していない可能性があるのに、結果にアクセスしようとする
// const result = promise; // result は Promise オブジェクト自体になる
// console.log("結果は Promise オブジェクト:", result); // 問題その2
promise.then(() => console.log("遅れて delay が完了")); // 遅れて完了通知は来る
console.log("Bad example end"); // 問題その3: 非同期処理が終わる前に async 関数が終了したように見える
}
badExample();
“`
正しいasync関数は、重要な非同期操作には必ず await
を付けるべきです。
“`typescript
async function goodExample() {
console.log(“Good example start”);
await delay(1000); // await で完了を待つ
console.log(“delay が完了しました”); // delay 完了後に実行される
const result = await readFilePromise("some.txt"); // await でファイル読み込み完了を待つ
console.log("ファイル読み込み結果:", result); // ファイル読み込み完了後に実行される
console.log("Good example end"); // 全ての await が完了してから async 関数が論理的に終了
}
goodExample();
“`
TypeScriptは、await
可能な式(Promiseを返す式)に対して await
を付け忘れている可能性がある場合に警告を出すことができます。tsconfig.json
で "noImplicitAny": true
や "strict": true
を有効にすることで、このような問題を検出しやすくなります。
エラーハンドリングの徹底:全てのawaitにtry...catch
が必要か?
async関数内で複数の await
がある場合、それぞれの await
の周りに個別の try...catch
ブロックを記述する必要はありません。async関数全体を一つの try...catch
で囲むことで、その中の任意の await
式がRejectedされた場合に捕捉できます。
“`typescript
async function sequentialProcessingWithSingleCatch() {
try {
const data1 = await readFilePromise(“file1.txt”); // エラーの可能性
console.log(“ファイル1 OK”);
const data2 = await readFilePromise("file2.txt"); // エラーの可能性
console.log("ファイル2 OK");
const data3 = await readFilePromise("file3.txt"); // エラーの可能性
console.log("ファイル3 OK");
} catch (error: any) {
// file1, file2, file3 のいずれかの読み込みでエラーが発生した場合、ここに捕捉される
console.error("シーケンシャル処理中にエラー:", error.message);
// 最初のエラーが発生した時点で、残りの await は実行されない
}
}
“`
これはPromiseチェーンの .catch()
と同様に、エラー処理を一箇所に集約できるため、コードが読みやすくなります。ただし、特定のエラーだけを個別に処理したい場合などは、部分的に try...catch
を使用することも可能です。
並列処理の活用:不要な直列処理を避ける
async/awaitでコードを書くと、つい全ての非同期処理を await
で直列に書いてしまいがちです。しかし、複数の独立した非同期処理がある場合は、Promise.all()
などを使用して並列に実行することを検討しましょう。これにより、アプリケーションのパフォーマンスを大幅に向上させることができます。
“`typescript
// 悪い例:不要な直列処理
async function inefficientParallel() {
const user = await fetchUser(123); // ユーザーデータを待つ
const products = await fetchProducts(user.preferredCategory); // 製品データを待つ
const ads = await fetchAds(); // 広告データを待つ
// …処理…
}
// 良い例:並列処理を活用
async function efficientParallel() {
// ユーザーデータと広告データは独立して取得可能
const userPromise = fetchUser(123);
const adsPromise = fetchAds();
// 両方のPromiseが完了するのを待つ
const [user, ads] = await Promise.all([userPromise, adsPromise]);
// ユーザーデータが取得できた後、それを使って製品データを取得
const products = await fetchProducts(user.preferredCategory);
// ...処理...
}
“`
efficientParallel
の例では、fetchUser
と fetchAds
は並列に実行されるため、全体の実行時間が短縮されます。fetchProducts
は fetchUser
の結果に依存するため、これは直列に実行する必要があります。
async関数からの戻り値:常にPromiseを返すことを理解する
async関数は、たとえ同期的な値を返したり、何も返さなかったり(undefinedを返したり)する場合でも、常にPromiseを返します。このPromiseが、同期的な値を解決したPromise (Promise.resolve(value)
) となるか、undefinedを解決したPromise (Promise.resolve(undefined)
) となるか、あるいは例外を拒否したPromise (Promise.reject(error)
) となるかは、async関数内の処理によって決まります。
この特性を理解しておくと、async関数を他のPromiseベースの関数と組み合わせる際に混乱が少なくなります。async関数はPromiseを返すので、その結果を .then()
, .catch()
, .finally()
で処理したり、他のasync関数内で await
したりできます。
Callback Hell回避:コールバックをasync/await化する方法
レガシーなコールバックベースのAPIをasync/awaitで扱いたい場合、それらのAPIをPromiseでラップすることで、async/awaitの恩恵を受けることができます。
例えば、Node.jsの古いコールバックベースのファイル読み込みAPI (fs.readFile
) をPromise化する場合:
“`typescript
import * as fs from ‘fs’;
function readFileCallback(filePath: string, callback: (err: NodeJS.ErrnoException | null, data: Buffer) => void): void {
fs.readFile(filePath, callback);
}
// コールバックベースのAPIをPromiseでラップする関数
function readFilePromiseWrapped(filePath: string): Promise
return new Promise((resolve, reject) => {
readFileCallback(filePath, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
// Promiseラッパーを使った async/await での処理
async function readFileSyncWrapped() {
try {
const data = await readFilePromiseWrapped(“my_legacy_file.txt”);
console.log(“ファイル内容 (ラップ済み):”, data.toString());
} catch (error: any) {
console.error(“ファイル読み込みエラー (ラップ済み):”, error.message);
}
}
readFileSyncWrapped();
“`
Node.jsの多くの組み込みモジュールには、最初からPromiseを返す代替APIが用意されています(例: fs.promises
)。これらのAPIを使うことで、自分でラップする必要なくasync/awaitを使用できます。
“`typescript
import * as fsPromises from ‘fs/promises’;
async function readFileUsingFsPromises() {
try {
const data = await fsPromises.readFile(“my_file.txt”);
console.log(“ファイル内容 (fs.promises):”, data.toString());
} catch (error: any) {
console.error(“ファイル読み込みエラー (fs.promises):”, error.message);
}
}
readFileUsingFsPromises();
“`
Promiseチェーンとの比較:async/awaitが適している場面、Promiseチェーンが適している場面
async/awaitはPromiseのシンタックスシュガーであり、両者は同じ非同期処理モデルに基づいています。どちらを使用するかは、多くの場合、コードのスタイルや特定の状況によって決まります。
-
async/awaitが適している場面:
- 非同期処理が直列に実行され、かつ後続の処理が前の処理の結果に依存する場合。
- 複雑な条件分岐やループを含む非同期処理。
if
,for
,while
などの同期的な制御フロー構文をそのまま使用できるため、コードが非常に読みやすくなります。 try...catch
によるエラーハンドリングを同期処理と同じように書きたい場合。- 非同期処理が同期処理と混ざっている場合(例えば、非同期処理の結果を使って同期的な計算を行い、その結果を次の非同期処理に渡すなど)。
-
Promiseチェーンが適している場面:
- 非同期処理の連鎖が比較的単純で、各ステップの結果を次のステップに渡すだけの線形的な処理。
- 複数のPromiseをまとめて処理し、その結果をさらに
.then()
で処理する場合(Promise.all().then(...)
のような形)。 - 特定の理由で async 関数を使用できない場合(例:古い環境でasync/awaitのダウンパイルが利用できない場合)。
一般的には、async/awaitの方が多くのケースでコードの可読性を向上させるため推奨されますが、Promiseチェーンも十分に理解しておくことは重要です。また、両者を組み合わせて使用することも可能です。async関数はPromiseを返すため、async関数を呼び出した結果に対して.then()
, .catch()
を付けることは自然なパターンです。
“`typescript
async function performOperation(): Promise
await delay(1000);
return “Operation Complete”;
}
// async 関数呼び出しの結果を Promise チェーンで処理する
performOperation()
.then(result => {
console.log(“async 関数からの結果を then で処理:”, result);
})
.catch(error => {
console.error(“async 関数からのエラーを catch で処理:”, error);
});
“`
デバッグ:非同期スタックトレース
非同期処理におけるエラーのデバッグは、同期処理よりも難しい場合があります。エラーが発生した際に、元の非同期処理がどこから開始されたのかを追跡するのが困難なためです。
幸い、モダンなJavaScriptエンジン(V8など)やNode.js、ブラウザの開発者ツールは、「非同期スタックトレース」をサポートしています。これにより、Promiseやasync/awaitを跨いだ関数呼び出しのスタックを追跡できるようになり、非同期コードのデバッグが大幅に改善されています。
エラーが発生した際には、開発者ツールのコンソールやNode.jsのエラー出力で、非同期スタックトレースを確認してみましょう。これにより、非同期処理がどのように連鎖してエラーに至ったかをより正確に把握できます。
実際の応用例
async/awaitは様々な非同期シナリオで活用されています。いくつかの代表的な例を見てみましょう。
ネットワークリクエスト(fetch
API)
WebブラウザやNode.js(バージョン18以降のグローバルfetchやライブラリ)で非同期にネットワークリソースを取得する fetch
APIはPromiseを返すため、async/awaitと非常に相性が良いです。
“`typescript
async function fetchJsonData
try {
const response = await fetch(url);
if (!response.ok) {
// HTTPエラーの場合はRejectedなPromiseを返す
throw new Error(`HTTP error! status: ${response.status}`);
}
// JSONデータを非同期にパースし、Promise<T>を返す
const data: T = await response.json();
return data;
} catch (error: any) {
console.error("Fetch error:", error);
// ネットワークエラーやパースエラーなどの場合
throw error; // エラーを再スローして呼び出し元で捕捉させる
}
}
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
async function loadPost(postId: number) {
const url = https://jsonplaceholder.typicode.com/posts/${postId}
;
try {
const post = await fetchJsonData
console.log(投稿 ${postId}:
, post.title);
} catch (error: any) {
console.error(投稿 ${postId} の取得に失敗:
, error.message);
}
}
loadPost(1);
loadPost(9999); // 存在しないIDでエラーを発生させる
“`
ファイルI/O(Node.jsのfs.promises
)
Node.js環境では、fs.promises
APIを使用することで、ファイルシステム操作をPromiseベースで行え、async/awaitと組み合わせて同期的なコードのように記述できます。
“`typescript
import * as fs from ‘fs/promises’;
async function readAndWriteFile(inputPath: string, outputPath: string) {
try {
// ファイルを非同期に読み込む
const data = await fs.readFile(inputPath, { encoding: ‘utf-8’ });
console.log("${inputPath}" の内容を読み込みました
);
// 読み込んだ内容を加工(例: 全て大文字に変換)
const processedData = data.toUpperCase();
// 加工した内容を非同期に書き込む
await fs.writeFile(outputPath, processedData, { encoding: 'utf-8' });
console.log(`加工した内容を "${outputPath}" に書き込みました`);
} catch (error: any) {
console.error("ファイル処理中にエラーが発生しました:", error.message);
}
}
// テストファイルを作成 (同期的に)
fs.writeFile(“input.txt”, “Hello, world!\nThis is a test file.”, { encoding: ‘utf-8’ })
.then(() => readAndWriteFile(“input.txt”, “output.txt”))
.catch(console.error);
“`
データベース操作
多くのモダンなデータベースドライバーやORM (Object-Relational Mapper) は、非同期操作のためにPromiseベースのAPIを提供しています。これにより、async/awaitを使用してデータベース操作を直感的に記述できます。
``typescript
Unknown query: ${sql}`);
// 擬似的なデータベースクライアント(Promiseベースのメソッドを持つと仮定)
const db = {
connect: async (): Promise<void> => {
console.log("DB接続中...");
await delay(500); // 接続時間をシミュレート
console.log("DB接続完了");
},
query: async (sql: string, params?: any[]): Promise<any[]> => {
console.log("クエリ実行:", sql);
await delay(1000); // クエリ実行時間をシミュレート
// 擬似的な結果を返す
if (sql.includes("SELECT * FROM users")) {
return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
} else if (sql.includes("INSERT")) {
console.log("挿入成功 (擬似)");
return []; // INSERT/UPDATEなどは結果が空配列や成功ステータスなど
}
throw new Error(
},
close: async (): Promise
console.log(“DB切断中…”);
await delay(300); // 切断時間をシミュレート
console.log(“DB切断完了”);
}
};
async function databaseExample() {
await db.connect(); // 接続を待つ
try {
// SELECTクエリを実行し、結果を待つ
const users = await db.query("SELECT * FROM users");
console.log("ユーザー一覧:", users);
// INSERTクエリを実行し、完了を待つ
await db.query("INSERT INTO users (name) VALUES (?)", ["Charlie"]);
console.log("新しいユーザーを挿入しました");
} catch (error: any) {
console.error("データベース操作中にエラー:", error.message);
} finally {
// finally ブロックで確実に切断処理を行う
await db.close(); // 切断を待つ
}
}
databaseExample();
“`
UIイベントハンドリング(非同期処理の結果を反映)
Webアプリケーションでは、ユーザーのアクション(ボタンクリックなど)に応じて非同期処理を実行し、その結果をUIに反映させることがよくあります。async/awaitは、イベントハンドラー内でこの処理を分かりやすく記述するのに役立ちます。
“`typescript
// HTML要素があると仮定:
const loadDataBtn = document.getElementById(‘loadDataBtn’);
const dataDisplay = document.getElementById(‘dataDisplay’);
// 擬似的なデータ取得関数
async function fetchRandomFact(): Promise
dataDisplay!.textContent = ‘Loading…’; // ロード中表示
await delay(2000); // ネットワーク遅延をシミュレート
const facts = [
“The shortest war in history lasted 38 to 45 minutes.”,
“A group of owls is called a parliament.”,
“The highest mountain on Mars is called Olympus Mons.”
];
const fact = facts[Math.floor(Math.random() * facts.length)];
if (Math.random() > 0.9) { // 10%の確率でエラーをシミュレート
throw new Error(“Failed to fetch fact.”);
}
return fact;
}
loadDataBtn?.addEventListener(‘click’, async () => {
console.log(“ボタンがクリックされました”);
try {
const fact = await fetchRandomFact(); // 非同期処理の完了を待つ
dataDisplay!.textContent = fact; // UIを更新
} catch (error: any) {
dataDisplay!.textContent = Error: ${error.message}
; // エラー表示
console.error(“UI更新エラー:”, error);
}
console.log(“イベントハンドラ終了 (非同期処理の結果反映済み)”);
});
// async/await は、イベントハンドラ自体を async にすることで使用可能になる
// addEventListener は async 関数をコールバックとして受け取れる
“`
イベントハンドラー関数を async
にすることで、その中で await
を使用できるようになります。これにより、非同期操作の完了を待ってからUIを更新したり、エラーメッセージを表示したりといった処理を、同期的なコードと同じように記述できます。
まとめ
本記事では、TypeScriptにおける非同期処理の重要性から始まり、Callback Hell、Promise、そしてasync/awaitへと至る歴史的な流れをたどりました。特にasync/awaitについては、その基本的な構文 (async
および await
) から、エラーハンドリング、複数の非同期処理の扱い(直列・並列)、非同期イテレーター、トップレベルawait、そしてTypeScriptの型システムとの連携まで、詳細に解説しました。
async/awaitは、Promiseをより人間が理解しやすい「同期的な見た目」で書くための強力なシンタックスシュガーです。これにより、非同期コードの可読性、記述量、そしてメンテナンス性が劇的に向上します。Callback Hellから解放されたPromiseも素晴らしい改善でしたが、async/awaitはさらに一歩進んで、多くのシナリオで非同期処理をほぼ同期処理と変わらない感覚で扱えるようにしました。
TypeScriptを使用することで、async/awaitによる非同期処理はさらに型安全になります。async関数の戻り値型は Promise<T>
として適切に扱われ、await
した結果も T
型として型チェックされるため、非同期処理の結果を安全に使用できます。
async/awaitは現代のTypeScript/JavaScript開発において、非同期処理を扱うための標準的な手法と言えます。ネットワーク通信、ファイルI/O、データベースアクセスなど、時間のかかる操作を伴うほとんど全てのアプリケーションでその真価を発揮します。
この記事で解説した内容を理解し、実践することで、async/awaitを使ったクリーンで堅牢な非同期コードを書けるようになるでしょう。非同期処理は一見難しく感じるかもしれませんが、async/awaitをマスターすれば、それは強力な味方となり、よりモダンで効率的なアプリケーション開発が可能になります。
async/awaitは単なる構文の変更ではなく、非同期処理に対する考え方を変えるツールです。ぜひ、積極的にあなたのプロジェクトに取り入れて、その恩恵を享受してください。
これで、TypeScript 非同期処理 async/await 完全ガイドは終了です。お読みいただきありがとうございました。