JavaScriptの非同期処理:Promise・async/await・fetchとエラー処理
JavaScriptの非同期処理をPromiseとasync/awaitで学習。fetchのHTTPエラー、JSONの検証、AbortControllerによる中断、finallyの後始末を解説します。
第5回では、処理の完了を待つPromiseとasync/awaitを学びます。ローカルのJSONファイルを読み込み、HTTPエラー、形式の検証、タイムアウトを扱う例へ進みます。Node.jsで動かすコードを中心に説明します。
Promiseとasync/awaitの基本
完了後の値をawaitで受け取る
Promiseは、将来の成功または失敗を表すオブジェクトです。async関数はPromiseを返し、awaitはその完了を待って結果を受け取ります。awaitで待っている間、JavaScript全体を同期的なループで停止させるわけではありません。
promise-basics.mjsを作成します。
const wait = (milliseconds) => new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
async function createMessage(name) {
await wait(20);
return `${name}の処理が完了`;
}
const results = await Promise.all([
createMessage('A'),
createMessage('B'),
]);
console.log(results); // ['Aの処理が完了', 'Bの処理が完了']
Promise.allとallSettledを使い分ける
Promise.allはすべて成功すると入力順に結果を返し、いずれかが失敗すると拒否されます。ただし、すでに開始したほかの処理を自動キャンセルしません。成功・失敗をすべて記録したい場合はPromise.allSettledを使います。
HTTPとJSONの失敗を分けて扱う
fetch-json.mjsを作成する
サポート中のNode.js LTSのfetchを使います。fetchはHTTP 404などでも通常はResponseを返すため、response.okを確認します。応答をJSONとして解析できても、必要なデータ形状かどうかは別途確認します。
export async function fetchJson(url, { signal } = {}) {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTPエラー:${response.status}`);
}
return response.json();
}
export function parseBooks(value) {
if (!Array.isArray(value)) {
throw new TypeError('本の配列が必要です。');
}
return value.map((book) => {
if (!book || typeof book.id !== 'string' || typeof book.title !== 'string') {
throw new TypeError('本のidとtitleは文字列にしてください。');
}
return { id: book.id, title: book.title };
});
}
books.jsonを用意する
同じ作業フォルダーへbooks.jsonを保存します。
[
{ "id": "javascript", "title": "JavaScriptの基本" },
{ "id": "classes", "title": "クラスの設計" }
]
AbortControllerで待ち時間を制限する
fetch-demo.mjsを作成する
5秒後に通信を中断するタイマーを用意し、処理が終わったらfinallyでタイマーを解除します。この中断はクライアント側の待機を止めるもので、サーバーで実行済みの操作を取り消す保証ではありません。
import { fetchJson, parseBooks } from './fetch-json.mjs';
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const data = await fetchJson('http://127.0.0.1:8080/books.json', {
signal: controller.signal,
});
const books = parseBooks(data);
console.log(books.map((book) => book.title));
} catch (error) {
if (controller.signal.aborted) {
console.error('待ち時間の上限に達したため中断しました。');
} else {
console.error(error instanceof Error ? error.message : String(error));
}
} finally {
clearTimeout(timer);
}
サーバーと実行プログラムを分ける
books.jsonのあるフォルダーでローカルHTTPサーバーを起動します。Python 3が利用できる場合は次のコマンドを使えます。
python3 -m http.server 8080 --bind 127.0.0.1
別のターミナルで同じ作業フォルダーに移動して実行します。
node fetch-demo.mjs
よくある間違いを避ける
forEachのasync関数を外側で待たない
forEachはコールバックが返すPromiseをまとめて待ちません。順番に待つならfor...ofとawait、同時に進めるならmapでPromise配列を作ってPromise.allなどへ渡します。
大量の処理を一度に開始しない
Promise.allへ渡す前に関数を呼べば、その時点で処理が開始されます。数万件を一度に開始すると、接続数やメモリーの問題につながります。連載最終回では、実行する関数をキューへ預けて同時実行数を制限します。
ブラウザーとNode.jsの制約を区別する
ブラウザーから別オリジンへ通信する場合はCORSなどの制約があります。認証キーをブラウザーのJavaScriptへ直接置いても秘密にはできません。この例は認証不要のローカルJSONを使っています。
練習と参考資料
失敗を意図的に起こす
- URLを存在しないファイルへ変えてHTTPエラーを確認します。
- JSONを壊して解析エラーを確認します。
- 本の
titleを数値へ変更してデータ検証を確認します。 - 中断はローカルの遅い応答や、事前に中断したsignalを使って確認できます。
次はクラスで状態と処理をまとめる
クラス・継承・thisで、データを操作するルールをクラスにまとめます。