【Javascript】今から始めるPromiseとasync/await – 現場のコードで見かける困ったを解決

「コールバック地獄から抜け出したい…」 「エラー処理どこまでやればいいの?」

最近のJavaScriptプロジェクトでは当たり前のように使われているPromiseとasync/await。しかし、エラーハンドリングの方法や、並列処理の実装など、使いこなせていない部分もあるのではないでしょうか。

この記事では、現場でよく遭遇する「困った」シチュエーションに焦点を当て、実践的な解決方法をお伝えします。特にAPIリクエストの処理やエラーハンドリングでよく使うパターンを中心に、明日から使える知識を解説していきます。

実践的なユースケースと解決策

APIリクエストの処理

直列処理(順次実行)

複数のAPIリクエストを順番に処理する必要がある場合、async/awaitを使うことで見通しの良いコードが書けます:

javascript
// Promiseチェーンを使用した場合
function getUserData(userId) {
    return fetchUser(userId)
        .then(user => fetchUserPosts(user.id))
        .then(posts => fetchPostComments(posts[0].id))
        .catch(error => {
            console.error('Error:', error);
            throw error;
        });
}

// async/awaitを使用した場合
async function getUserData(userId) {
    try {
        const user = await fetchUser(userId);
        const posts = await fetchUserPosts(user.id);
        const comments = await fetchPostComments(posts[0].id);
        return comments;
    } catch (error) {
        console.error('Error:', error);
        throw error;
    }
}

並列処理(Promise.all)

複数のリクエストを同時に実行する場合は、Promise.allPromise.allSettledを使用します:

javascript
async function fetchAllUserData(userIds) {
    try {
        // 全てのリクエストが成功する必要がある場合
        const users = await Promise.all(
            userIds.map(id => fetchUser(id))
        );

        // 一部の失敗を許容する場合
        const results = await Promise.allSettled(
            userIds.map(id => fetchUser(id))
        );

        const successfulUsers = results
            .filter(result => result.status === 'fulfilled')
            .map(result => result.value);

        return successfulUsers;
    } catch (error) {
        console.error('Some requests failed:', error);
        throw error;
    }
}

エラーハンドリング

エラー処理は非同期処理で最も重要な部分の1つです。適切なエラーハンドリングを実装しましょう:

javascript
class APIError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.name = 'APIError';
        this.statusCode = statusCode;
    }
}

async function fetchWithErrorHandling(url) {
    try {
        const response = await fetch(url);
        
        if (!response.ok) {
            throw new APIError(
                'API request failed', 
                response.status
            );
        }

        return await response.json();
    } catch (error) {
        if (error instanceof APIError) {
            // APIエラーの場合の処理
            handleAPIError(error);
        } else if (error.name === 'AbortError') {
            // リクエストがキャンセルされた場合の処理
            handleAbortError(error);
        } else {
            // その他のエラー(ネットワークエラーなど)
            handleUnexpectedError(error);
        }
        throw error;
    }
}

実務での活用パターン

リトライ処理の実装

一時的なエラーに対応するため、リトライ処理を実装することがあります:

javascript
async function fetchWithRetry(url, retries = 3, delay = 1000) {
    for (let i = 0; i < retries; i++) {
        try {
            return await fetch(url);
        } catch (error) {
            if (i === retries - 1) throw error;
            
            // 待機時間を指数的に増やす
            await new Promise(resolve => 
                setTimeout(resolve, delay * Math.pow(2, i))
            );
            
            console.log(`Retrying... (${i + 1}/${retries})`);
        }
    }
}

タイムアウトの設定

長時間のリクエストを制御するためのタイムアウト処理:

javascript
function timeout(ms) {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject(new Error('Timeout'));
        }, ms);
    });
}

async function fetchWithTimeout(url, ms = 5000) {
    try {
        const result = await Promise.race([
            fetch(url),
            timeout(ms)
        ]);
        return result;
    } catch (error) {
        if (error.message === 'Timeout') {
            console.error('Request timed out');
        }
        throw error;
    }
}

よくある落とし穴と対処法

await忘れの問題

awaitを忘れると、Promise自体が返されてしまい、意図しない動作になります:

javascript
// BAD: awaitを忘れている
async function getUser(id) {
    try {
        const user = fetchUser(id);  // Promiseが返される
        console.log(user.name);  // undefined
    } catch (error) {
        console.error(error);
    }
}

// GOOD: awaitを使用
async function getUser(id) {
    try {
        const user = await fetchUser(id);
        console.log(user.name);  // 正しく値が取得できる
    } catch (error) {
        console.error(error);
    }
}

メモリリークの防止

特にReactなどのフロントエンドフレームワークでは、コンポーネントのアンマウント時にリクエストをキャンセルする必要があります:

javascript
function UserProfile({ userId }) {
    const [user, setUser] = useState(null);

    useEffect(() => {
        const controller = new AbortController();

        async function fetchUser() {
            try {
                const response = await fetch(
                    `/api/users/${userId}`,
                    { signal: controller.signal }
                );
                const data = await response.json();
                setUser(data);
            } catch (error) {
                if (error.name === 'AbortError') {
                    // キャンセルされた場合は何もしない
                    return;
                }
                console.error(error);
            }
        }

        fetchUser();

        // クリーンアップ関数でリクエストをキャンセル
        return () => controller.abort();
    }, [userId]);

    // ...
}

実務でのベストプラクティス

TypeScriptでの型定義

TypeScriptを使用する場合、非同期関数の戻り値の型を明示的に定義することで、より安全なコードが書けます:

typescript
interface User {
    id: number;
    name: string;
    email: string;
}

async function fetchUser(id: number): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
        throw new Error('User not found');
    }
    return response.json();
}

レビュー時のチェックポイント

  1. エラーハンドリング
    • 適切なtry-catchの範囲
    • エラーの種類に応じた処理
    • ユーザーへのフィードバック
  2. リソースの解放
    • AbortControllerの使用
    • クリーンアップ処理の実装
  3. パフォーマンス
    • 不必要な直列化がないか
    • キャッシュの活用

Promiseとasync/awaitは、非同期処理を扱う上で欠かせない機能です。適切に使用することで、可読性が高く保守しやすいコードを書くことができます。特にエラーハンドリングとリソース管理には注意を払い、プロジェクトに適した使い方を見つけてください。