TypeScriptのPromiseとasync/await:戻り値の型・並行処理・エラー処理を学ぶ
非同期関数のPromise型、awaitとtry/catch、Promise.allの使い分けを解説。unknownのエラー処理や、forEachでawaitを待てない問題も、実行できる例で確認します。
APIから記事を読み込む処理では、関数を呼び出しても、その場では結果がそろわないことがあります。Promiseは、後で成功または失敗する処理の結果を扱う仕組みです。この記事では関数とオブジェクト型の基礎を前提に、戻り値の型、複数処理の待ち方、失敗の扱い方を学びます。
【1. Promise<Article>とArticleの違い】
Article は記事データそのもの、Promise<Article> は記事データで成功する非同期処理を表します。async 関数はPromiseを返し、await は成功した値を取り出します。失敗したPromiseをawaitすると、その場所から例外処理に進みます。仕様は MDNのasync function(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) を参照してください。
Promiseの型引数は、成功した値の型です。Promise<Article> と書いても、失敗する値が必ずErrorになるとは限りません。また、async を付けるだけでCPUを使う重い計算が別スレッドに移るわけではありません。
【2. 成功と失敗を一つのコードで確認する】
次のloadArticleは、通信なしで動く学習用の関数です。IDが正しい場合は記事を返し、0の場合は失敗します。遅延やネットワーク速度を再現する例ではなく、非同期処理の型と制御の流れを確認するために使います。
ファイル: articles.ts
type Article = { id: number; title: string };
// 通信部分を置き換えた学習用の関数。実際のHTTP通信は行わない。
async function loadArticle(id: number): Promise<Article> {
if (id < 1) {
throw new Error("記事IDは1以上にしてください");
}
return { id, title: `記事${id}` };
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : "不明なエラー";
}
async function main(): Promise<void> {
const first = await loadArticle(1);
console.log(first.title);
// 結果に依存関係がない処理を開始し、両方の完了を待つ。
const [second, third] = await Promise.all([
loadArticle(2),
loadArticle(3),
]);
console.log(`${second.title}, ${third.title}`);
try {
await loadArticle(0);
} catch (error: unknown) {
console.log(`失敗: ${errorMessage(error)}`);
}
}
main().catch((error: unknown) => {
console.error(`処理を終了しました: ${errorMessage(error)}`);
});
出力は順に「記事1」「記事2, 記事3」「失敗: 記事IDは1以上にしてください」です。main() が返すPromiseにもcatchを付けているので、途中で予期しない失敗が起きてもエラーメッセージを表示する経路があります。
【3. catchではunknownを確認してから使う】
JavaScriptでは文字列やオブジェクトもthrowできるため、catchした値の message を無条件に読むと危険です。例では instanceof Error で確認し、Errorでないときは代わりのメッセージを返しています。strict設定でのcatch変数の扱いは useUnknownInCatchVariables(https://www.typescriptlang.org/tsconfig/useUnknownInCatchVariables.html) で確認できます。
上のコードはエラーを表示して終了する学習用の設計です。実務では「失敗画面を表示する」「呼び出し元へ投げ直す」「再試行する」など、利用者に何を伝えるかまで決めます。メッセージだけを記録して成功扱いにすると、障害に気付きにくくなります。
【4. 逐次処理とPromise.allを使い分ける】
先に取得した結果が次の入力になる場合は、順にawaitします。互いに依存しない取得は、Promise.allでまとめて待つことができます。結果の配列は完了順ではなく、渡した順に対応します。どれかが失敗するとPromise.allも失敗しますが、すでに開始した残りの処理を自動で取り消すわけではありません。詳しくは MDNのPromise.all(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) を参照してください。
大量のIDを一度にmapして通信すると、接続数やAPIの制限に達する場合があります。Promise.all自体に同時実行数を制限する機能はないため、件数が多い処理では上限を決める仕組みも検討します。各処理の成功・失敗をすべて集めたい場合は Promise.allSettled() が候補です。
【5. forEach(async ...)で外側は待機しない】
ids.forEach(async id => { await loadArticle(id); }); と書いても、forEachはコールバックが返すPromiseをまとめて待ちません。順に処理したいなら for...of の中でawaitし、まとめて開始するなら await Promise.all(ids.map(id => loadArticle(id))) のように、待つ対象を明示します。MDNのforEach解説(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) にも、この注意点が記載されています。
【6. 実際のHTTP通信に置き換える前に】
TypeScriptの型注釈は、外部サービスから返るJSONを検証しません。as Article で断定するだけでは、titleが存在しないレスポンスを見逃します。通信の成否を確かめ、受け取った値の構造を検証してからArticleとして扱ってください。今回は型と非同期処理に集中するため、HTTPクライアントや入力検証の実装は扱いません。
【実行方法と練習問題】
新しい作業用フォルダに articles.ts と次の設定を保存します。TypeScriptをインストールした環境で npx tsc -p .、続いて node dist/articles.js を実行してください。
ファイル: tsconfig.json
{"compilerOptions":{"target":"ES2022","module":"NodeNext","strict":true,"outDir":"dist","types":[],"lib":["ES2022","DOM"]},"include":["*.ts"]}
練習としてPromise.allの片方のIDを0に変え、外側のcatchに進むことを確認しましょう。次に、2つの処理の成否を別々に表示するにはどんな戻り値や処理が必要か考えてください。
【よくある質問】
【awaitはアプリ全体を停止しますか?】
その非同期関数の続きを、待っているPromiseの結果が出るまで中断します。他の実行可能な処理は進められます。ただし、awaitの前後で長い同期処理を実行すれば、その処理が動くスレッドを占有します。
【async関数で戻り値を書かない場合は?】
結果のデータを返さない処理は Promise<void> と表せます。データを返さなくても失敗することはあるため、呼び出し側でawaitやcatchを使って完了と失敗を扱います。
【参考資料】
・MDN: async function(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
・MDN: Promise.all()(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)
・TSConfig: useUnknownInCatchVariables(https://www.typescriptlang.org/tsconfig/useUnknownInCatchVariables.html)