3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

async/awaitって結局何?非同期処理をわかりやすく解説

Posted at

はじめに

「コールバック地獄って聞いたことある」
「Promiseは何となくわかるけど、async/awaitって必要?」
「非同期処理って、結局どういうこと?」

JavaScriptで開発してると必ず出てくる非同期処理。今回は「なんで非同期処理が必要なの?」から「async/awaitでどう楽になるの?」まで、丁寧に解説します!

そもそも同期処理と非同期処理って何?

JavaScriptは「シングルスレッド」で動く言語です。つまり、一度に一つの処理しかできません。でも、もしすべての処理を順番に待っていたら、Webアプリはとても遅くなってしまいます。そこで非同期処理が必要になるのです。

同期処理 = レジに並ぶ

スーパーのレジを想像してください
レジが1つしかなく、店員さんも一人。前のお客さんのレジが終わるまで、次のお客さんはずっと待たなければいけません。

// 同期処理の例
console.log('1人目のお会計');  // 3分かかる
console.log('2人目のお会計');  // 2分かかる
console.log('3人目のお会計');  // 4分かかる
// 合計: 9分かかる

このJavaScriptコードでは、各処理が順番に実行されます。console.logが実行される順番は必ず上から下へ。
もし各処理が時間がかかる処理だった場合、前の処理が終わるまで次の処理は実行されません。これが「同期処理」です。

非同期処理 = ファミレスの注文

ファミレスの厨房を想像してください
ウェイターは注文を受けたら、厨房に伝えて、すぐに次のお客さんの注文を受けに行きます。料理ができるのを待っている必要はありません。

// 非同期処理の例
注文1を受ける  料理開始  そのまま次へ
注文2を受ける  料理開始  そのまま次へ
注文3を受ける  料理開始  そのまま次へ
// 全部同時進行!早い!

コードで表現すると、「注文を受ける」処理を開始したら、その完了を待たずに次の「注文を受ける」処理を開始します。
これにより、複数の処理が同時並行で進み、全体の処理時間が大幅に短縮されます。これが「非同期処理」です。

なんで非同期処理が必要なの?

JavaScriptのイベントループ

JavaScriptには「イベントループ」という仕組みがあります。これは、ユーザーのクリックやキーボード入力、APIのレスポンスなど、様々なイベントを処理するためのメカニズムです。

もしすべてが同期処理だったら、APIのレスポンスを待っている間、ユーザーは一切操作できなくなってしまいます。

ユーザー体験が最悪になるから

同期処理だけだと...

// ❌ 悪い例(同期処理)
button.addEventListener('click', () => {
  // 3秒かかるAPI呼び出し
  const data = fetchDataSync(); // この間、画面が固まる!
  
  // やっと動く
  console.log(data);
});

結果:

  • ボタンを押した瞬間から3秒間、ブラウザが完全に固まります
  • スクロールできない、他のボタンを押せない、テキスト入力できない
  • ユーザーは「フリーズした?アプリが壊れた?」と不安になります
  • 最悪の場合、ユーザーはページを離脱してしまいます

非同期処理なら...

// ✅ 良い例(非同期処理)
button.addEventListener('click', async () => {
  // API呼び出し中も画面は動く
  const data = await fetchData();
  console.log(data);
});

結果:

  • ボタンを押した後も、画面の操作が可能です
  • ローディングアニメーションを表示できます
  • ユーザーは「処理中」であることがわかり、安心して待てます
  • 処理中でも他の操作(キャンセルボタンを押すなど)ができます

非同期処理の歴史と進化

1. コールバック時代(昔)

// データ取得 → ユーザー情報取得 → 投稿取得
getData(function(data) {
  getUser(data.userId, function(user) {
    getPosts(user.id, function(posts) {
      console.log(posts);
      // どんどん右に行く...これがコールバック地獄
    });
  });
});

問題点:

  • ネストが深くなる: 処理が増えるたびに、コードが右に右にインデントされていきます
  • エラー処理が複雑: 各コールバックごとにエラー処理を書く必要があります
  • デバッグが困難: どこでエラーが起きたか追いにくく、処理の流れが把握しづらい
  • コードの再利用が難しい: ネストしたコールバックを別の場所で使うのが大変

2. Promise時代(ちょっと前)

// Promiseチェーンで書く
getData()
  .then(data => getUser(data.userId))
  .then(user => getPosts(user.id))
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

改善点:

  • ネストが解消: コールバック地獄から解放され、フラットな構造に
  • エラー処理の一元化: .catch()メソッドで、すべてのエラーを一箇所で処理
  • チェーン可能: Promiseを連鎖させることで、処理の流れが明確に
  • でもまだ問題が...: thenの中でreturnを忘れると意図しない動作になる、コードが横に長くなる

3. async/await時代(今)

// async/awaitで書く
async function fetchUserPosts() {
  try {
    const data = await getData();
    const user = await getUser(data.userId);
    const posts = await getPosts(user.id);
    console.log(posts);
  } catch (error) {
    console.error(error);
  }
}

最高のポイント:

  • 同期処理のような書き方: 上から下へ、順番に処理が流れるように見えます
  • 可読性の向上: 誰が見ても処理の流れが一目でわかります
  • デバッグが簡単: ブレークポイントを設定して、ステップ実行が可能
  • エラー処理が直感的: try/catchで同期処理と同じようにエラー処理
  • 学習コストが低い: 同期処理を知っていれば、すぐに理解できます

async/awaitの基本

asyncとは?

「この関数は非同期処理を含みますよ」という宣言

asyncキーワードを関数の前に付けると、その関数は自動的にPromiseを返すようになります。
つまり、async関数の中では、awaitを使って非同期処理を「同期処理のように」書けるようになります。

// asyncを付けると、必ずPromiseを返す関数になる
async function myFunction() {
  return 'Hello'; // 自動でPromiseでラップされる
}

// 使う時
myFunction().then(result => console.log(result)); // 'Hello'

awaitとは?

「この処理が終わるまで待って」という命令

awaitはPromiseが解決(resolve)されるまで、その場で処理を一時停止します。
しかし、JavaScriptのイベントループ自体はブロックされないので、他の処理(ユーザーのクリックなど)は継続して処理されます。

async function fetchData() {
  // awaitを付けると、Promiseが解決するまで待つ
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  
  return data; // dataが取得できてから返す
}

重要なルール:

  • awaitは必ずasync関数の中でしか使えません!
  • awaitを使うことで、Promiseの結果を直接取得できます
  • awaitを使わないと、Promiseオブジェクトそのものが返ってきます

実践的な使い方

1. APIからデータを取得

実際のWebアプリケーションで最もよく使われるパターンです。サーバーからデータを取得し、画面に表示する処理です。

// ユーザー情報を取得する関数
async function getUserInfo(userId) {
  try {
    // APIを呼ぶ
    const response = await fetch(`/api/users/${userId}`);
    
    // レスポンスをチェック
    if (!response.ok) {
      throw new Error('ユーザーが見つかりません');
    }
    
    // JSONに変換
    const user = await response.json();
    
    return user;
  } catch (error) {
    console.error('エラー:', error.message);
    return null;
  }
}

// 使い方
async function displayUser() {
  const user = await getUserInfo(123);
  if (user) {
    console.log(`名前: ${user.name}`);
  }
}

2. 複数の処理を並列実行

複数のAPIを同時に呼び出す必要がある場合、順番に実行すると時間がかかります。
Promise.allを使うことで、すべての非同期処理を同時に開始し、すべてが完了するのを待つことができます。

// ❌ 遅い方法(順番に実行)
async function fetchSequential() {
  const user = await fetchUser();      // 1秒
  const posts = await fetchPosts();    // 1秒
  const comments = await fetchComments(); // 1秒
  // 合計: 3秒
}

// ✅ 速い方法(並列実行)
async function fetchParallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
  // 合計: 1秒(全部同時に実行)
}

3. エラーハンドリング

ネットワークエラーやサーバーエラーなど、非同期処理では様々なエラーが発生する可能性があります。
適切なエラーハンドリングは、ユーザー体験を大幅に向上させます。

// 個別にエラー処理
async function robustFetch() {
  let user, posts;
  
  // ユーザー取得(エラーでも続行)
  try {
    user = await fetchUser();
  } catch (error) {
    console.log('ユーザー取得失敗:', error);
    user = { name: 'ゲスト' };
  }
  
  // 投稿取得(エラーでも続行)
  try {
    posts = await fetchPosts();
  } catch (error) {
    console.log('投稿取得失敗:', error);
    posts = [];
  }
  
  return { user, posts };
}

4. ローディング表示の実装

ReactやVueなどのフレームワークでは、データ取得中にローディング表示を出すのが一般的です。
これにより、ユーザーはアプリケーションが処理中であることを理解できます。

// Reactでの例
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 非同期処理を実行
    async function loadUser() {
      try {
        setLoading(true);
        const userData = await fetchUser(userId);
        setUser(userData);
      } catch (err) {
        setError('ユーザー情報の取得に失敗しました');
      } finally {
        setLoading(false);
      }
    }
    
    loadUser();
  }, [userId]);

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (!user) return null;

  return <div>こんにちは{user.name}さん</div>;
}

よくある間違いと解決法

1. awaitを忘れる

最もよくあるミスです。awaitを忘れると、Promiseオブジェクトがそのまま返ってきて、
意図しない動作になります。デバッグ時に「Promise {}」という表示を見たら、
まずwaitを忘れている可能性が高いです。

// ❌ 間違い
async function badExample() {
  const data = fetch('/api/data'); // awaitを忘れた!
  console.log(data); // Promise {<pending>} が表示される
}

// ✅ 正解
async function goodExample() {
  const response = await fetch('/api/data');
  const data = await response.json();
  console.log(data); // 実際のデータが表示される
}

2. forEachでawaitを使う

forEachメソッドは非同期処理を待ちません。これはJavaScriptの仕様で、
forEachのコールバック関数がasyncであっても、その完了を待たずに次のループに進んでしまいます。

// ❌ 間違い(forEachは非同期処理を待たない)
const urls = ['url1', 'url2', 'url3'];
urls.forEach(async (url) => {
  const data = await fetch(url); // 待たずに次のループへ
});

// ✅ 正解(for...ofを使う)
for (const url of urls) {
  const data = await fetch(url); // ちゃんと待つ
}

// ✅ または map + Promise.all
const promises = urls.map(url => fetch(url));
const results = await Promise.all(promises);

3. トップレベルでawaitを使う

JavaScriptのファイルのトップレベル(関数の外)ではawaitが使えません。
ただし、Node.js 14.8以降や最新のブラウザでは、ESModule形式でトップレベルawaitがサポートされています。

// ❌ 間違い(トップレベルでは使えない)
const data = await fetch('/api/data');

// ✅ 正解1(async関数でラップ)
async function main() {
  const data = await fetch('/api/data');
}
main();

// ✅ 正解2(即時実行関数)
(async () => {
  const data = await fetch('/api/data');
})();

実用的なパターン集

リトライ処理

ネットワークエラーは一時的なものであることが多く、再試行することで成功する場合があります。
以下は、指定回数まで自動リトライする実装例です。

async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url);
      if (response.ok) {
        return await response.json();
      }
    } catch (error) {
      console.log(`試行 ${i + 1} 失敗:`, error.message);
      
      // 最後の試行でなければ待機
      if (i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
    }
  }
  throw new Error('最大リトライ回数を超えました');
}

タイムアウト処理

APIコールが長時間応答しない場合、タイムアウトを設定して処理を中断することが重要です。
AbortControllerを使うことで、fetchリクエストをキャンセルできます。

async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  
  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeout);
  
  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    clearTimeout(timeoutId);
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('タイムアウトしました');
    }
    throw error;
  }
}

キャッシュ処理

同じデータを何度も取得するのは非効率的です。
一度取得したデータをキャッシュすることで、APIコールの数を減らし、
アプリケーションのパフォーマンスを向上させることができます。

const cache = new Map();

async function fetchWithCache(url) {
  // キャッシュがあれば返す
  if (cache.has(url)) {
    console.log('キャッシュから取得');
    return cache.get(url);
  }
  
  // なければフェッチ
  console.log('APIから取得');
  const response = await fetch(url);
  const data = await response.json();
  
  // キャッシュに保存
  cache.set(url, data);
  
  return data;
}

デバッグのコツ

非同期処理のデバッグは難しいと思われがちですが、いくつかのテクニックを知っていれば、
効率的に問題を特定できます。

1. console.logで確認

async function debugExample() {
  console.log('処理開始');
  
  const data = await fetchData();
  console.log('データ取得完了:', data);
  
  const processed = await processData(data);
  console.log('処理完了:', processed);
  
  return processed;
}

2. Chrome DevToolsの活用

ブラウザの開発者ツールは非同期処理のデバッグに非常に役立ちます:

  • Networkタブ:

    • APIのリクエストとレスポンスを確認
    • リクエストのタイミングや所要時間を分析
    • エラーステータスを確認(404、500など)
  • Consoleタブ:

    • エラーメッセージとスタックトレースを確認
    • console.logの出力を確認
    • Promiseの状態を確認
  • Sourcesタブ:

    • async関数にブレークポイントを設定
    • awaitの前後でステップ実行
    • 変数の値をリアルタイムで確認

3. エラースタックトレース

async function betterError() {
  try {
    await someAsyncFunction();
  } catch (error) {
    console.error('エラー発生箇所:', error.stack);
    throw error; // 再スロー
  }
}

まとめ

async/awaitは「非同期処理を同期処理のように書ける」魔法の構文です。

この記事で学んだことを振り返ると:

覚えておくポイント:

  • async: 非同期関数の宣言
  • await: Promiseの完了を待つ
  • try/catch: エラー処理
  • Promise.all: 並列実行

使いどころ:

  • API通信
  • ファイル読み書き
  • データベース操作
  • 時間のかかる処理全般

あなたのコードはこう変わります:

  • 可読性の向上: ネストが深いコールバックから、直線的で読みやすいコードへ
  • メンテナンス性の向上: 新しい開発者が参加しても、すぐにコードを理解できます
  • デバッグの容易さ: ステップ実行で簡単に処理の流れを追えます
  • エラーハンドリングの簡素化: try/catchで統一的にエラー処理ができます

最初は「Promiseで十分」と思うかもしれませんが、async/awaitを使い始めると、もう戻れなくなります。

最後に覚えておくべきこと:

非同期処理は最初は難しく感じるかもしれませんが、以下のポイントさえ押さえておけば大丈夫です:

  1. 「時間がかかる処理にはawait」 を付ける
  2. awaitを使う関数にはasyncを付ける
  3. エラー処理はtry/catchで囲む
  4. 並列処理にはPromise.allを使う

この4つをマスターすれば、非同期処理は完璧に理解していると言えるでしょう!

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?