はじめに ~ JavaScript の非同期を理解してみる

# JavaScript 非同期処理クイズ

突然ですが、皆さんは以下のようなコードが何をしているかわかりますか？
各コードを実行したときのコンソール出力を考えてみてください。(JavaScript ES2017 以降を利用想定)

**例 1 非同期処理の基本**

```javascript
console.log("start");
setTimeout(() => {
  console.log("コールバック");
}, 1000);
console.log("end");
```

<details><summary>回答</summary>

```console
start
end
コールバック
```

ポイント
これは、基本的な非同期処理を行う WebAPI の動作を確認するための例です。

- `setTimeout` の結果として、1000ms 後に `コールバック` が実行されます。
- 内部的には、`setTimeout` の処理は Web APIs に移譲され、1000ms 後に `コールバック` が `Task Queue` に追加されます。
- そのため、`console.log` により `start` と `end` がまず出力された後に `コールバック` が出力されます。

</details>

**例 2 タスクキューの動作**

```javascript
console.log("start");

setTimeout(() => {
  console.log("0秒後のコールバック");
}, 0);

setTimeout(() => {
  console.log("3秒後のコールバック");
}, 3000);

setTimeout(() => {
  console.log("1秒後のコールバック");
}, 1000);

console.log("end");
```

<details><summary>回答</summary>

```console
start
end
0秒後のコールバック
1秒後のコールバック
3秒後のコールバック
```

ポイント
これも例 1 と同じく基本的な非同期処理を行う WebAPI の動作を確認するための例です。

- `setTimeout` は、指定した時間後に Callback 関数をキューに登録します。
- 指定が `0ms` だとしても、Task を新しく作成してキューに登録するため、その処理は、今の関数の同期処理が完了した後に実行されます。

</details>

**例 3 async/await の動作**

```javascript
async function main() {
  console.log("main start");
  await Promise.resolve();
  console.log("main end");
}
console.log("start");
main();
console.log("end");
```

<details><summary>回答</summary>

```console
start
main start
end
main end
```

ポイント
これは、`Promise & async/await` の非同期処理を理解しているかを確認するための例です。

- `main` 関数内の `await Promise.resolve()` によって、後続の処理はマイクロタスクキューに登録され、`main` 関数は一時中断されます。
- `main()` を呼び出すとすぐに `Promise` を返すため、関数本体の実行が一旦止まり、その間に `console.log("end")` が実行されて `end` が出力されます。
- イベントループによって、マイクロタスクキューに登録されていた `"main end"` の処理が、現在のスタックが空になったあとに実行されます。

</details>

**例 4 マイクロタスクとマクロタスク**

```javascript
setTimeout(() => {
  console.log("setTimeout");
}, 0);

Promise.resolve().then(() => {
  console.log("promise");
});

console.log("start");
```

<details><summary>回答</summary>

```console
start
promise
setTimeout
```

ポイント
これは、WebAPI の非同期処理 と`Promise`(マイクロタスク)の実行優先度の違いを確認するための例です。

- `setTimeout` は、通常の非同期処理 WebAPI であり、Callback 関数はタスクキューに登録されます。
- `Promise.resolve().then(() => { console.log("promise"); })` は、`Promise` のマイクロタスクキューに登録されます。
- 内部処理では、マイクロタスクキューは通常のタスクキューよりも優先度が高いため、`setTimeout` よりも先に `promise` が実行されます。

</details>

**例 5 基本的なエラーハンドリング**

```javascript
async function fetchUser(id) {
  if (id < 0) throw new Error("Invalid ID");
  return { id, name: "User" };
}

console.log("start");
fetchUser(-1)
  .then((user) => console.log("Success:", user))
  .catch((err) => console.log("Error:", err.message))
  .finally(() => console.log("Complete"));
console.log("end");
```

<details><summary>回答</summary>

```console
start
end
Error: Invalid ID
Complete
```

ポイント
これは、基本的なエラーハンドリングを理解しているかを確認するための例です。

- `fetchUser` 関数内で `id` が負の場合にエラーを投げています。
- そのため、`fetchUser` 関数の結果を `then` で受け取ることができず、`catch` でエラーを処理します。
- `finally` は、最後に必ず実行されます。

</details>

**例 6 並列実行**

```javascript
function task(name, delay) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(name + " 完了");
      resolve(name);
    }, delay);
  });
}

Promise.all([task("Task1", 500), task("Task2", 1000)]).then(([res1, res2]) => {
  console.log("全タスク完了:", res1, res2);
});
```

<details><summary>回答</summary>

```console
Task1 完了
Task2 完了
全タスク完了: Task1 Task2
```

ポイント
これは、`Promise.all` による非同期処理の並列実行を理解しているかを確認するための例です。

- `Promise.all` は、複数の非同期処理を並列実行し、全ての処理が完了したら、その結果を配列で返します。
- 一つでも失敗すると、`Promise.all` は失敗します。

```javascript
// 一部が失敗し Promise.all が失敗する例

function task(name, delay) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(name + " 完了");
      resolve(name);
    }, delay);
  });
}

function failedTask(name, delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(name + " 完了");
      reject(name);
    }, delay);
  });
}

Promise.all([
  task("Task1", 500),
  task("Task2", 1000),
  failedTask("Task3", 1500),
])
  .then(([res1, res2, res3]) => {
    console.log("全タスク完了:", res1, res2, res3);
  })
  .catch(() => {
    console.log("failed");
  });
```

</details>

**例 7 複数の非同期処理の並列実行**

```javascript
// モックのfetch関数
const mockFetch = (url) => {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        // api1は即座に失敗
        if (url.includes("api1")) {
          reject(new Error("Network Error"));
        }
        // api2は1秒後に成功
        else if (url.includes("api2")) {
          resolve({
            ok: true,
            json: () => Promise.resolve({ data: "Success from api2" }),
          });
        }
        // api3は2秒後に404
        else if (url.includes("api3")) {
          resolve({
            ok: false,
            status: 404,
            statusText: "Not Found",
          });
        }
      },
      url.includes("api2") ? 1000 : 0
    );
  });
};

// 実行関数
function fetchWithFallback(urls, timeout = 3000) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error("All requests timed out")), timeout);
  });

  const fetchPromises = urls.map((url) =>
    mockFetch(url)
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .catch((err) => {
        console.log(`${url} failed:`, err.message);
        return Promise.reject(err);
      })
  );

  return Promise.race([
    Promise.any(fetchPromises).catch((err) =>
      Promise.reject(new Error("All requests failed"))
    ),
    timeoutPromise,
  ]);
}

// 以下3つのURLへの連続的なfetchを実行 (Mock済みです)
const urls = [
  "https://api1.example.com/data", // 即座に失敗
  "https://api2.example.com/data", // 1秒後に成功
  "https://api3.example.com/data", // 404エラー
];

console.log("開始");
fetchWithFallback(urls, 2000)
  .then((data) => console.log("Success:", data))
  .catch((err) => console.log("Error:", err.message))
  .finally(() => console.log("Complete"));
```

<details><summary>回答</summary>

```console
開始
https://api1.example.com/data failed: Network Error
https://api3.example.com/data failed: HTTP 404
Success: {data: "Success from api2"}
Complete
```

ポイント
これは、`Promise.race` と `Promise.any` の違いを理解しているかを確認するための例です。

- `Promise.race` は、複数の非同期処理を並列実行し、最初に解決/失敗した処理の結果を返します。
- `Promise.any` は、複数の非同期処理を並列実行し、最初に成功した処理の結果を返します。

url1, 3 は失敗しますが、 `Promise.any` は 成功した url2 の結果を返します。そのため、`Promise.race` は成功として、url2 の結果を返します。これは各リクエストの結果をログに残しつつ、実行したリクエストのどれか一つでも成功したときに、その結果を利用するというケースを模擬しています。

</details>

**例 8 再帰的な非同期処理**

\* 例 8 は実際に実行することは避けてください。
以下の 8-a, 8-b の違いを回答してください。

8-a

```javascript
let loopDepth = 0;
async function loop() {
  loopDepth++;
  console.log("loop depth", loopDepth);
  await loop();
}

async function main() {
  try {
    await loop();
  } catch (error) {
    console.error(error);
  }
}

main();
```

8-b

```javascript
let loopDepth = 0;
function loop() {
  loopDepth++;
  console.log("loop depth", loopDepth);
  Promise.resolve().then(() => loop());
}

async function main() {
  try {
    loop();
  } catch (error) {
    console.error(error);
  }
}

// Run the main function
main();
```

<details><summary>回答</summary>

```console
// 8-a
Uncaught (in promise) RangeError: Maximum call stack size exceeded

// 8-b
loop depth 1
loop depth 2
loop depth 3
loop depth 4
loop depth 5 ...
```

ポイント
これは再帰処理の中で`Promise & async/await` におけるマイクロタスクの登録タイミングとスタックの動作を理解しているかを確認するための例です。

- 8-a では、`await` を使っており、`loop`関数の結果を待ち続けるため、関数がスタックに積まれていき、スタックオーバーフローが発生します。
- 8-b では、`Promise.resolve().then(() => loop())` によって、`loop`関数自体を Callback としてマイクロタスクキューに登録しています。そのため、スタックに関数が積まれることなく、無限に呼び出しされます。

</details>

**例 9 リトライロジック**

```javascript
// モックfetch
let callCount = 0;
function mockFetch() {
  return new Promise((resolve, reject) => {
    callCount++;
    console.log(`fetch called: ${callCount}回目`);

    if (callCount === 3) {
      resolve({ ok: true, data: "成功データ" });
    } else {
      reject(new Error(`${callCount}回目の呼び出しで失敗`));
    }
  });
}

function withRetry(promiseFn, maxRetries = 3) {
  return new Promise(async (resolve, reject) => {
    for (let i = 0; i < maxRetries; i++) {
      try {
        const result = await promiseFn();
        return resolve(result);
      } catch (err) {
        if (i === maxRetries - 1) reject(err);
        await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
      }
    }
  });
}

console.log("開始");
withRetry(() => mockFetch())
  .then((result) => console.log("成功:", result))
  .catch((err) => console.log("最終的な失敗:", err.message));
```

<details><summary>回答</summary>

```console
開始
fetch called: 1回目
fetch called: 2回目
fetch called: 3回目
成功: {ok: true, data: "成功データ"}
```

ポイント
これは、典型的なリトライロジックを実装している例です。API のリクエストが失敗した場合に、一定時間後に再試行するというケースを模擬しています。内容としては、特段難しくないですが、非常によくあるケースとして掲載しています。

- `withRetry` 関数は、`promiseFn` を最大 `maxRetries` 回リトライします。
- リトライ回数が `maxRetries` 回を超えた場合に、エラーを投げます。

</details>

### 答え合わせ

➡️ 全てわかった方: おめでとうございます。この記事を読んでも得るものは少ないので時間をお返しします。よければ "いいね"をいただけると励みになります。

➡️ 6~8 問解けた方:　おめでとうございます。非同期についてかなり理解されていると思います。周りにJavaScript の非同期処理を学習している方がいたら、この記事をお勧めしていただけるとありがたいです。

➡️ 3~5 問解けた方:　おめでとうございます。非同期について基本的な挙動は理解されていると思うので、復習の意を込めて記事を読んでいただけるとありがたいです。

➡️ 1~2 問解けた方:　おめでとうございます。非同期処理が実現する仕組みを理解することで、より深い理解ができると思います。ぜひ記事を読んでいただけるとありがたいです。

➡️ 1 問もわからなかった方: ありがとうございます。数ヶ月前の私と同じです。

恥ずかしながら自分は最初、jQuery deferred や async/await が、何をしているのかちゃんと理解していませんでした。そこで、JavaScript の非同期について調べ始めました。

調べ始めてわかったのが、JavaScript の非同期処理は、他言語でのマルチスレッド処理や並列処理と呼ばれるものとは全く異なる流れで処理されているということでした。そして、その非同期処理の流れを理解するためには、JavaScript 自体について理解を深める必要があるということを理解しました。

このシリーズは、私個人が JavaScript について勉強した内容をまとめたものです。自分のための備忘録として、そして自分と同じように非同期処理がわかっていない人が、何となくわかったつもりになれるような内容を目指しています。
(できる限り正確を期するようにしていますが、もし誤りなどあれば、コメントいただけるととても嬉しいです。)

## Table of Contents

- 第 0 章 はじめに (この記事)
- 第 1 章 [JavaScript の歴史と特徴](https://qiita.com/ZawaP/items/87147c38dd2ec754b86e)
- 第 2 章 [JavaScript 実行モデル (ECMAScript が定める実行モデル)](https://qiita.com/ZawaP/items/cff743fc724e72ee3b8c)
- 第 3 章 [ブラウザでの JavaScript の実行 (HTML Living Standard が定める実行モデル)](https://qiita.com/ZawaP/items/16cafaba3b60bf2b2d71)
- 第 4 章 [実装レベルで見る JavaScript の実行 (V8 & Chromium)](https://qiita.com/ZawaP/items/d185453455dd80cc2b68)
- 第 5 章 [非同期処理の基本 (Callback)](https://qiita.com/ZawaP/items/4f5914bfb5fbb67538b7)
- 第 6 章 [非同期処理の基本 (jQUery Deferred)](https://qiita.com/ZawaP/items/ffdde0a83f322002642c)
- 第 7 章 [Promise と async/await](https://qiita.com/ZawaP/items/1362951ebc86b76dcea1)
- 第 8 章 [まとめ](https://qiita.com/ZawaP/items/97bbefa34b83d55de91e)

参考資料

[HTML Living Standard](https://html.spec.whatwg.org/)
[ECMAScript® 262 Language Specification](https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html)
[What the heck is the event loop anyway? | Philip Roberts | JSConf EU](https://youtu.be/8aGhZQkoFbQ?si=yUTzAM0b9Doirb6S)
[Further Adventures of the Event Loop - Erin Zimmer - JSConf EU 2018](https://www.youtube.com/watch?v=u1kqx6AenYw)
[JavaScript execution model](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model)