非同期処理の進化 – コールバック地獄からの解放

「Ajax通信のコールバック、入れ子が深くなってきて辛い…」 「Promiseは分かるけど、async/awaitってどう使えばいいの?」 「$.ajaxの代わりにfetchを使うって聞いたけど、使い方が分からない」

前回はDOM操作のモダンな書き方を学びました。今回は、非同期処理の書き方の進化について見ていきましょう。jQueryの$.ajaxから、最新のasync/awaitまで、段階的に理解していきます。

コールバック地獄を体験する

まず、従来のjQueryでの非同期処理を見てみましょう:

JS
// jQuery: ユーザー情報を取得して、その友達リストを取得し、最新の投稿を表示する
$.ajax({
    url: '/api/user/1',
    success: function(user) {
        console.log('ユーザー取得完了');
        $.ajax({
            url: '/api/friends/' + user.id,
            success: function(friends) {
                console.log('友達リスト取得完了');
                $.ajax({
                    url: '/api/posts/' + friends[0].id,
                    success: function(posts) {
                        console.log('投稿取得完了');
                        $('#result').text(posts[0].title);
                    },
                    error: function(error) {
                        console.error('投稿取得エラー:', error);
                    }
                });
            },
            error: function(error) {
                console.error('友達リスト取得エラー:', error);
            }
        });
    },
    error: function(error) {
        console.error('ユーザー取得エラー:', error);
    }
});

このコードの問題点:

  • ネストが深くなり、可読性が低下
  • エラーハンドリングが各レベルで必要
  • 並列処理や条件分岐が複雑になりやすい

Promiseによる改善

JS
// fetch APIを使用した最初のアプローチ
fetch('/api/user/1')
    .then(response => response.json())
    .then(user => {
        console.log('ユーザー取得完了');
        return fetch('/api/friends/' + user.id);
    })
    .then(response => response.json())
    .then(friends => {
        console.log('友達リスト取得完了');
        return fetch('/api/posts/' + friends[0].id);
    })
    .then(response => response.json())
    .then(posts => {
        console.log('投稿取得完了');
        document.querySelector('#result').textContent = posts[0].title;
    })
    .catch(error => {
        console.error('エラーが発生しました:', error);
    });

Promiseの特徴:

  • チェーンによる直線的な記述が可能
  • エラーハンドリングの一元化
  • 非同期処理の合成が容易

async/awaitによる更なる改善

JS
// async/awaitを使用した最新のアプローチ
async function fetchUserData() {
    try {
        const userResponse = await fetch('/api/user/1');
        const user = await userResponse.json();
        console.log('ユーザー取得完了');

        const friendsResponse = await fetch('/api/friends/' + user.id);
        const friends = await friendsResponse.json();
        console.log('友達リスト取得完了');

        const postsResponse = await fetch('/api/posts/' + friends[0].id);
        const posts = await postsResponse.json();
        console.log('投稿取得完了');

        document.querySelector('#result').textContent = posts[0].title;
    } catch (error) {
        console.error('エラーが発生しました:', error);
    }
}

// 関数の実行
fetchUserData();

async/awaitのメリット:

  • 同期処理のように読みやすい
  • try/catchによる直感的なエラーハンドリング
  • デバッグがしやすい

実践的なパターン

並列リクエストの処理

JS
// jQuery
$.when(
    $.ajax('/api/users'),
    $.ajax('/api/posts'),
    $.ajax('/api/comments')
).then(function(users, posts, comments) {
    console.log(users[0], posts[0], comments[0]);
});

// Modern JavaScript
async function fetchAllData() {
    try {
        const [users, posts, comments] = await Promise.all([
            fetch('/api/users').then(r => r.json()),
            fetch('/api/posts').then(r => r.json()),
            fetch('/api/comments').then(r => r.json())
        ]);
        console.log(users, posts, comments);
    } catch (error) {
        console.error('エラー:', error);
    }
}

ポイント:

  • Promise.allで複数のリクエストを並列実行
  • 分割代入で結果を簡潔に取得
  • エラーは一括でキャッチ可能

リクエストのタイムアウト設定

JS
// jQuery
$.ajax({
    url: '/api/data',
    timeout: 5000,
    success: function(data) {
        console.log(data);
    },
    error: function(xhr, status, error) {
        if (status === 'timeout') {
            console.log('タイムアウトしました');
        }
    }
});

// Modern JavaScript
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;
    }
}

ポイント:

  • AbortControllerでリクエストのキャンセルが可能
  • タイムアウト時間を柔軟に設定可能
  • エラーの種類を区別して処理可能

リトライ処理の実装

JS
// Modern JavaScript
async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const response = await fetch(url);
            return await response.json();
        } catch (error) {
            if (i === retries - 1) throw error;
            console.log(`リトライ ${i + 1}/${retries}`);
            await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
        }
    }
}

リトライ処理のポイント:

  • エラー発生時に一定回数リトライ
  • 指数バックオフで待機時間を増加
  • 最終リトライ後はエラーをスロー

よくある非同期処理のパターン

データの検証付きフェッチ

JS
// Modern JavaScript
async function fetchValidatedData(url) {
    const response = await fetch(url);
    
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    
    if (!data.success) {
        throw new Error(data.error || 'データの検証に失敗しました');
    }
    
    return data.result;
}

ポイント:

  • HTTPステータスのチェック
  • レスポンスデータの検証
  • エラーメッセージの明確化

まとめ

非同期処理は、モダンJavaScriptで大きく進化しました:

  • コールバック → Promise → async/await
  • より直感的な記述が可能に
  • エラーハンドリングが簡潔に
  • 高度な制御が容易に

次回は「モジュール化への第一歩 – import/export を理解する」について解説し、コードの構造化について学んでいきましょう。