モダンJavaScriptの非同期処理完全ガイド:Promise、async/awaitからWeb Workersまで

JavaScriptにおける非同期処理は、モダンなWebアプリケーション開発において欠かせない技術です。この記事では、基本的なPromiseの概念から実践的な実装パターンまで、包括的に解説します。

1. 非同期処理の基礎概念

なぜ非同期処理が必要なのか

JavaScriptはシングルスレッドで動作するため、重い処理を同期的に実行するとUIがブロックされてしまいます。非同期処理により、以下のメリットが得られます:

  • レスポンシブなUI:ユーザーインターフェースが固まることなく、スムーズな操作が可能
  • 効率的なリソース利用:I/O待機時間中に他の処理を実行
  • スケーラビリティの向上:複数の処理を並行して実行

従来のコールバック地獄からの脱却

// 従来のコールバック地獄の例
getData(function(a) {
    getMoreData(a, function(b) {
        getEvenMoreData(b, function(c) {
            getFinalData(c, function(d) {
                // 処理が深くネストしてしまう
                console.log(d);
            });
        });
    });
});

2. Promiseの詳細解説

Promiseの基本構造

Promiseは非同期処理の結果を表現するオブジェクトで、以下の3つの状態を持ちます:

  • Pending(保留中):初期状態、まだ実行されていない
  • Fulfilled(履行済み):処理が正常に完了
  • Rejected(拒否済み):処理が失敗
const myPromise = new Promise((resolve, reject) => {
    // 非同期処理
    setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) {
            resolve("処理が成功しました");
        } else {
            reject(new Error("処理が失敗しました"));
        }
    }, 1000);
});

myPromise
    .then(result => console.log(result))
    .catch(error => console.error(error));

Promise.allとPromise.allSettledの使い分け

// Promise.all - 全て成功した場合のみ成功
const fetchAllData = async () => {
    try {
        const [users, posts, comments] = await Promise.all([
            fetch('/api/users').then(res => res.json()),
            fetch('/api/posts').then(res => res.json()),
            fetch('/api/comments').then(res => res.json())
        ]);
        return { users, posts, comments };
    } catch (error) {
        console.error('いずれかのAPIでエラーが発生:', error);
    }
};

// Promise.allSettled - 個別の成功/失敗を処理
const fetchDataWithErrorHandling = async () => {
    const results = await Promise.allSettled([
        fetch('/api/users').then(res => res.json()),
        fetch('/api/posts').then(res => res.json()),
        fetch('/api/comments').then(res => res.json())
    ]);
    
    results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
            console.log(`API ${index} 成功:`, result.value);
        } else {
            console.log(`API ${index} 失敗:`, result.reason);
        }
    });
};

3. async/awaitの実践的な使い方

エラーハンドリングのベストプラクティス

// 包括的なエラーハンドリング
async function robustApiCall(url) {
    try {
        const response = await fetch(url);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        if (error instanceof TypeError) {
            console.error('ネットワークエラー:', error.message);
        } else if (error instanceof SyntaxError) {
            console.error('JSONパースエラー:', error.message);
        } else {
            console.error('その他のエラー:', error.message);
        }
        throw error; // 上位レイヤーでの処理のため再スロー
    }
}

// リトライ機能付きAPI呼び出し
async function apiCallWithRetry(url, maxRetries = 3) {
    let lastError;
    
    for (let i = 0; i < maxRetries; i++) {
        try {
            return await robustApiCall(url);
        } catch (error) {
            lastError = error;
            if (i < maxRetries - 1) {
                const delay = Math.pow(2, i) * 1000; // 指数バックオフ
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
    }
    
    throw new Error(`${maxRetries}回のリトライ後も失敗: ${lastError.message}`);
}

並行処理と逐次処理の使い分け

// 並行処理(高速だが負荷が高い)
async function processConcurrently(items) {
    const promises = items.map(async (item) => {
        return await processItem(item);
    });
    return await Promise.all(promises);
}

// 逐次処理(安全だが時間がかかる)
async function processSequentially(items) {
    const results = [];
    for (const item of items) {
        const result = await processItem(item);
        results.push(result);
    }
    return results;
}

// バッチ処理(並行数を制限)
async function processBatches(items, batchSize = 5) {
    const results = [];
    
    for (let i = 0; i < items.length; i += batchSize) {
        const batch = items.slice(i, i + batchSize);
        const batchResults = await Promise.all(
            batch.map(item => processItem(item))
        );
        results.push(...batchResults);
    }
    
    return results;
}

4. 高度な非同期パターン

カスタムPromiseの実装

class CustomPromise {
    constructor(executor) {
        this.state = 'pending';
        this.value = undefined;
        this.reason = undefined;
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];
        
        const resolve = (value) => {
            if (this.state === 'pending') {
                this.state = 'fulfilled';
                this.value = value;
                this.onFulfilledCallbacks.forEach(callback => callback(value));
            }
        };
        
        const reject = (reason) => {
            if (this.state === 'pending') {
                this.state = 'rejected';
                this.reason = reason;
                this.onRejectedCallbacks.forEach(callback => callback(reason));
            }
        };
        
        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }
    
    then(onFulfilled, onRejected) {
        return new CustomPromise((resolve, reject) => {
            if (this.state === 'fulfilled') {
                try {
                    const result = onFulfilled ? onFulfilled(this.value) : this.value;
                    resolve(result);
                } catch (error) {
                    reject(error);
                }
            } else if (this.state === 'rejected') {
                try {
                    const result = onRejected ? onRejected(this.reason) : this.reason;
                    resolve(result);
                } catch (error) {
                    reject(error);
                }
            } else {
                this.onFulfilledCallbacks.push((value) => {
                    try {
                        const result = onFulfilled ? onFulfilled(value) : value;
                        resolve(result);
                    } catch (error) {
                        reject(error);
                    }
                });
                
                this.onRejectedCallbacks.push((reason) => {
                    try {
                        const result = onRejected ? onRejected(reason) : reason;
                        resolve(result);
                    } catch (error) {
                        reject(error);
                    }
                });
            }
        });
    }
}

Generator関数を使った非同期制御

function* asyncGenerator() {
    try {
        const user = yield fetch('/api/user');
        const posts = yield fetch(`/api/posts/${user.id}`);
        const comments = yield fetch(`/api/comments/${posts[0].id}`);
        return { user, posts, comments };
    } catch (error) {
        console.error('Generator内でエラー:', error);
    }
}

async function runGenerator(generator) {
    const iterator = generator();
    let result = iterator.next();
    
    while (!result.done) {
        try {
            const data = await result.value;
            result = iterator.next(data.json ? await data.json() : data);
        } catch (error) {
            result = iterator.throw(error);
        }
    }
    
    return result.value;
}

5. Web Workersを使った重い処理の分離

メインスレッドをブロックしない計算処理

// worker.js
self.onmessage = function(e) {
    const { data, operation } = e.data;
    
    switch (operation) {
        case 'fibonacci':
            const result = fibonacci(data.n);
            self.postMessage({ result });
            break;
        
        case 'sort':
            const sorted = data.array.sort((a, b) => a - b);
            self.postMessage({ result: sorted });
            break;
    }
};

function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// main.js
class WorkerManager {
    constructor(workerScript) {
        this.worker = new Worker(workerScript);
        this.taskId = 0;
        this.pendingTasks = new Map();
        
        this.worker.onmessage = (e) => {
            const { taskId, result, error } = e.data;
            const task = this.pendingTasks.get(taskId);
            
            if (task) {
                this.pendingTasks.delete(taskId);
                if (error) {
                    task.reject(new Error(error));
                } else {
                    task.resolve(result);
                }
            }
        };
    }
    
    async execute(operation, data) {
        return new Promise((resolve, reject) => {
            const taskId = ++this.taskId;
            this.pendingTasks.set(taskId, { resolve, reject });
            
            this.worker.postMessage({
                taskId,
                operation,
                data
            });
        });
    }
    
    terminate() {
        this.worker.terminate();
    }
}

// 使用例
const workerManager = new WorkerManager('worker.js');

async function heavyCalculation() {
    try {
        const result = await workerManager.execute('fibonacci', { n: 40 });
        console.log('フィボナッチ計算結果:', result);
    } catch (error) {
        console.error('Worker実行エラー:', error);
    }
}

6. パフォーマンス最適化のテクニック

非同期処理のデバッグとプロファイリング

// 実行時間測定デコレータ
function measureTime(target, propertyName, descriptor) {
    const method = descriptor.value;
    
    descriptor.value = async function(...args) {
        const start = performance.now();
        try {
            const result = await method.apply(this, args);
            const end = performance.now();
            console.log(`${propertyName} 実行時間: ${end - start}ms`);
            return result;
        } catch (error) {
            const end = performance.now();
            console.log(`${propertyName} エラー (${end - start}ms):`, error);
            throw error;
        }
    };
    
    return descriptor;
}

class ApiService {
    @measureTime
    async fetchUserData(userId) {
        const response = await fetch(`/api/users/${userId}`);
        return response.json();
    }
}

// メモリリークを防ぐAbortController
class RequestManager {
    constructor() {
        this.activeRequests = new Set();
    }
    
    async fetchWithTimeout(url, timeout = 5000) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);
        
        this.activeRequests.add(controller);
        
        try {
            const response = await fetch(url, {
                signal: controller.signal
            });
            return await response.json();
        } finally {
            clearTimeout(timeoutId);
            this.activeRequests.delete(controller);
        }
    }
    
    cancelAllRequests() {
        this.activeRequests.forEach(controller => controller.abort());
        this.activeRequests.clear();
    }
}

7. 実践的な応用例

リアルタイム通信とWebSocket

class WebSocketManager {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.eventListeners = new Map();
    }
    
    async connect() {
        return new Promise((resolve, reject) => {
            this.ws = new WebSocket(this.url);
            
            this.ws.onopen = () => {
                this.reconnectAttempts = 0;
                resolve();
            };
            
            this.ws.onmessage = (event) => {
                try {
                    const data = JSON.parse(event.data);
                    this.emit(data.type, data.payload);
                } catch (error) {
                    console.error('メッセージパースエラー:', error);
                }
            };
            
            this.ws.onclose = () => {
                if (this.reconnectAttempts < this.maxReconnectAttempts) {
                    setTimeout(() => {
                        this.reconnectAttempts++;
                        this.connect();
                    }, Math.pow(2, this.reconnectAttempts) * 1000);
                }
            };
            
            this.ws.onerror = (error) => {
                reject(error);
            };
        });
    }
    
    on(event, callback) {
        if (!this.eventListeners.has(event)) {
            this.eventListeners.set(event, []);
        }
        this.eventListeners.get(event).push(callback);
    }
    
    emit(event, data) {
        const listeners = this.eventListeners.get(event);
        if (listeners) {
            listeners.forEach(callback => callback(data));
        }
    }
    
    send(type, payload) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify({ type, payload }));
        }
    }
}

まとめ

モダンJavaScriptの非同期処理は、Webアプリケーションの品質とパフォーマンスを大きく左右する重要な技術です。Promise、async/await、Web Workersなどの技術を適切に組み合わせることで、ユーザー体験を向上させ、保守性の高いコードを書くことができます。

今回紹介したパターンやテクニックを実際のプロジェクトで活用し、継続的に学習を深めていくことをお勧めします。非同期処理をマスターすることで、より高度なWebアプリケーション開発が可能になるでしょう。


よく参考にさせていただいているEngineerCompassさんのサイトは下記からどうぞ。

Engineer Compass -

コメント

タイトルとURLをコピーしました