POS開発 フロントエンド チートコード — 非同期

7. 非同期処理

コールバック → Promise → async/await の流れ

JavaScriptはシングルスレッドのため、時間のかかる処理(API呼び出し、タイマーなど)は非同期で実行される。async/awaitが最も読みやすく推奨。

setTimeout / setInterval

// 一定時間後に1回実行
const timerId = setTimeout(() => {
  console.log("3秒後に実行");
}, 3000);
clearTimeout(timerId);  // キャンセル

// 一定間隔で繰り返し実行
const intervalId = setInterval(() => {
  console.log("5秒ごとに実行");
}, 5000);
clearInterval(intervalId);  // 停止

Promise 基本

// Promiseの作成
const myPromise = new Promise((resolve, reject) => {
  const success = true;
  if (success) {
    resolve("成功データ");   // 成功時
  } else {
    reject(new Error("エラー"));  // 失敗時
  }
});

// then / catch / finally チェーン
myPromise
  .then(data => {
    console.log(data);       // "成功データ"
    return processData(data); // 次のthenに値を渡す
  })
  .then(result => {
    console.log(result);     // processDataの結果
  })
  .catch(err => {
    console.error(err);      // どこかでエラーが起きたら
  })
  .finally(() => {
    console.log("完了");      // 成功でも失敗でも実行
  });

async / await

// async関数: Promiseを返す関数を宣言
async function loadProducts() {
  const response = await fetch('/api/products');  // Promiseの完了を待つ
  const data = await response.json();              // これもPromise
  return data;
}

// アロー関数版
const loadProducts = async () => {
  const res = await fetch('/api/products');
  return res.json();
};

// 呼び出し側
const products = await loadProducts();  // トップレベルawait(モジュール内)

// または
loadProducts().then(products => {
  renderProducts(products);
});

エラーハンドリング

async function checkout(cartItems) {
  const loading = showLoading();
  try {
    const result = await submitOrder(cartItems);
    showSuccess("注文が完了しました");
    return result;
  } catch (error) {
    // エラーの種類で分岐
    if (error instanceof TypeError) {
      showError("ネットワークエラー: サーバーに接続できません");
    } else if (error.status === 400) {
      showError("入力内容に問題があります");
    } else {
      showError(`予期しないエラー: ${error.message}`);
    }
  } finally {
    hideLoading(loading);  // 成功でも失敗でも実行される
  }
}

Promise.all / Promise.allSettled / Promise.race

// Promise.all: 全て成功するまで待つ(1つでも失敗したら即catch)
try {
  const [users, products, orders] = await Promise.all([
    fetchUsers(),
    fetchProducts(),
    fetchOrders(),
  ]);
} catch (e) {
  // いずれか1つが失敗した場合
}

// Promise.allSettled: 全ての結果を取得(失敗しても止まらない)
const results = await Promise.allSettled([
  fetchUsers(),
  fetchProducts(),
]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  if (r.status === "rejected")  console.error(r.reason);
});

// Promise.race: 最初に完了(成功or失敗)したものを採用
const result = await Promise.race([
  fetchData(),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("タイムアウト")), 5000)
  ),
]);

非同期の逐次実行 vs 並列実行

// ❌ 逐次実行(遅い: A完了 → B完了 → C完了)
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();

// ✅ 並列実行(速い: A,B,Cを同時に開始)
const [a, b, c] = await Promise.all([
  fetchA(),
  fetchB(),
  fetchC(),
]);

// 配列の各要素を順番に非同期処理する場合
for (const item of items) {
  await processItem(item);  // 1つずつ順番に
}

8. Fetch API

GETリクエスト

// 基本的なGET
const response = await fetch('/api/products');
const data = await response.json();

// クエリパラメータ付き
const params = new URLSearchParams({
  category: "drink",
  limit: "10",
  page: "1",
});
const res = await fetch(`/api/products?${params}`);
const drinks = await res.json();

POSTリクエスト(JSON送信)

const response = await fetch('/api/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    items: cartItems,
    paymentMethod: "cash",
    total: 2500,
  }),
});

const result = await response.json();

PUT / DELETE

// PUT: 更新
await fetch(`/api/products/${id}`, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ price: newPrice }),
});

// DELETE: 削除
await fetch(`/api/products/${id}`, {
  method: 'DELETE',
});

レスポンスの処理

const response = await fetch(url);

// ステータス確認
response.ok;         // true (200-299の場合)
response.status;     // 200, 404, 500 など
response.statusText; // "OK", "Not Found" など

// ボディの取得(※1回しか読めない)
const json = await response.json();   // JSONとしてパース
const text = await response.text();   // テキストとして取得
const blob = await response.blob();   // バイナリ(画像など)

// レスポンスヘッダーの確認
response.headers.get('Content-Type');

エラーハンドリング

fetchは404や500でもrejectしない(ネットワークエラーのみreject)。response.okのチェックが必須。
async function apiFetch(url, options = {}) {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      // HTTPエラー(404, 500など)→ fetchはrejectしないので自分でthrow
      const errorBody = await response.text();
      throw new Error(`HTTP ${response.status}: ${errorBody}`);
    }

    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      // ネットワークエラー(サーバー未到達、DNS解決失敗など)
      console.error("ネットワークエラー:", error.message);
    } else {
      // HTTPエラーまたはJSONパースエラー
      console.error("APIエラー:", error.message);
    }
    throw error;
  }
}

FormDataで送信

// フォームから直接取得して送信
const form = document.getElementById('myForm');
const formData = new FormData(form);

// Content-Typeは自動設定されるので指定不要
await fetch('/api/upload', {
  method: 'POST',
  body: formData,
});

// FormDataの中身を確認
for (const [key, value] of formData) {
  console.log(`${key}: ${value}`);
}

// FormDataをオブジェクトに変換
const obj = Object.fromEntries(formData);

AbortController(リクエストのキャンセル)

const controller = new AbortController();

// タイムアウト付きfetch
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch('/api/data', {
    signal: controller.signal,
  });
  clearTimeout(timeoutId);
  return await res.json();
} catch (e) {
  if (e.name === 'AbortError') {
    console.log('リクエストがキャンセルされました');
  }
}

// 検索入力時の前回リクエストキャンセル
let currentController = null;
input.addEventListener('input', async (e) => {
  currentController?.abort();  // 前回のリクエストをキャンセル
  currentController = new AbortController();
  try {
    const res = await fetch(`/api/search?q=${e.target.value}`, {
      signal: currentController.signal,
    });
    const data = await res.json();
    renderResults(data);
  } catch (e) {
    if (e.name !== 'AbortError') throw e;
  }
});