JavaScript

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...ofawait、同時に進めるならmapでPromise配列を作ってPromise.allなどへ渡します。

大量の処理を一度に開始しない

Promise.allへ渡す前に関数を呼べば、その時点で処理が開始されます。数万件を一度に開始すると、接続数やメモリーの問題につながります。連載最終回では、実行する関数をキューへ預けて同時実行数を制限します。

ブラウザーとNode.jsの制約を区別する

ブラウザーから別オリジンへ通信する場合はCORSなどの制約があります。認証キーをブラウザーのJavaScriptへ直接置いても秘密にはできません。この例は認証不要のローカルJSONを使っています。

練習と参考資料

失敗を意図的に起こす

  • URLを存在しないファイルへ変えてHTTPエラーを確認します。
  • JSONを壊して解析エラーを確認します。
  • 本のtitleを数値へ変更してデータ検証を確認します。
  • 中断はローカルの遅い応答や、事前に中断したsignalを使って確認できます。

次はクラスで状態と処理をまとめる

クラス・継承・thisで、データを操作するルールをクラスにまとめます。

参考資料

y.
WRITTEN BY

y_ymo10

SEの部屋で、JavaScript・TypeScript・React.js・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →