JavaScriptのPromise.raceで競争条件を効率的に処理する方法

JavaScriptの非同期処理は、特に複雑なアプリケーションでは避けて通れない重要な要素です。その中でも、Promise.raceは、複数の非同期操作の中で最初に完了したものに基づいて次の処理を進めるための強力なツールです。これにより、競争条件(Race Condition)を効率的に処理することができます。本記事では、Promise.raceの基本概念から、具体的な使用例、エラーハンドリング、タイムアウト処理、ユーザーインターフェイスへの応用まで、詳しく解説していきます。Promise.raceの理解を深め、実際のプロジェクトで効果的に活用するための知識を身につけましょう。

目次

Promise.raceとは何か

Promise.raceは、JavaScriptのPromiseオブジェクトの一つであり、複数のPromiseの中で最初に完了(解決または拒否)したものを取得するためのメソッドです。このメソッドに渡されたPromiseの配列のうち、最初に結果が返ってきたPromiseの結果を取得し、後続の処理を進めます。Promise.raceは、複数の非同期操作の中で最も早く完了した操作に基づいて次の処理を決定する際に非常に有用です。

基本的な使い方

Promise.raceの基本的な使用方法は以下の通りです:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'first');
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'second');
});

Promise.race([promise1, promise2])
    .then(value => {
        console.log(value); // "second"
    })
    .catch(error => {
        console.error(error);
    });

この例では、promise2が最初に解決されるため、Promise.raceの結果として”second”が出力されます。promise1がその後に解決されても、Promise.raceの結果には影響しません。

用途と利点

Promise.raceは、特定の非同期処理が複数ある場合に最初に完了したものを優先するシナリオで役立ちます。例えば、複数のネットワークリクエストを並行して実行し、その中で最も早く応答を返すサーバーの結果を採用する場合などです。また、特定の処理が一定時間以内に完了しなければタイムアウトとみなす、といったタイムアウト処理にも適しています。

Promise.raceの使用例

Promise.raceを使用することで、複数の非同期操作のうち最初に完了したものに基づいて次の処理を進めることができます。ここでは、具体的なコード例を用いてPromise.raceの使用方法を解説します。

例1:複数の非同期タスクの競争

以下の例では、2つの非同期タスクを並行して実行し、最初に完了したタスクの結果を取得します。

const task1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Task 1 completed'), 300);
});

const task2 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Task 2 completed'), 200);
});

Promise.race([task1, task2])
    .then(result => {
        console.log(result); // "Task 2 completed"
    })
    .catch(error => {
        console.error(error);
    });

この例では、task2が200ミリ秒で完了し、task1は300ミリ秒かかります。したがって、Promise.racetask2の結果を返します。

例2:タイムアウト処理

次の例では、ネットワークリクエストが一定時間内に完了しない場合にタイムアウトエラーを発生させます。

const fetchData = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Data fetched successfully'), 500);
});

const timeout = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Request timed out')), 300);
});

Promise.race([fetchData, timeout])
    .then(result => {
        console.log(result); // This will not be called in this example
    })
    .catch(error => {
        console.error(error.message); // "Request timed out"
    });

この例では、fetchDataが500ミリ秒で完了し、timeoutは300ミリ秒でエラーを返します。Promise.raceは最初に完了したtimeoutの結果としてエラーを返します。

例3:ユーザーインターフェイスの応答性向上

最後に、Promise.raceを使ってユーザーインターフェイスの応答性を向上させる例です。

const loadImage1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Image 1 loaded'), 1000);
});

const loadImage2 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Image 2 loaded'), 700);
});

Promise.race([loadImage1, loadImage2])
    .then(result => {
        console.log(result); // "Image 2 loaded"
        // ユーザーに最初にロードされた画像を表示する
    })
    .catch(error => {
        console.error(error);
    });

この例では、2つの画像読み込み処理を並行して実行し、最初にロードされた画像をユーザーに表示します。loadImage2が700ミリ秒で完了するため、その結果が表示されます。

競争条件とは

競争条件(Race Condition)とは、複数のプロセスやスレッドが同時にリソースにアクセスし、結果がアクセスのタイミングによって変わる現象を指します。JavaScriptの非同期プログラミングにおいても、競争条件は重要な概念です。

競争条件の例

例えば、2つの非同期操作が同時に行われ、どちらが先に完了するかによって結果が異なる場合があります。以下はその一例です。

let sharedResource = 0;

const asyncTask1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        sharedResource += 1;
        resolve('Task 1 completed');
    }, 300);
});

const asyncTask2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        sharedResource *= 2;
        resolve('Task 2 completed');
    }, 200);
});

Promise.all([asyncTask1, asyncTask2])
    .then(results => {
        console.log(sharedResource); // 結果は異なる場合がある
    })
    .catch(error => {
        console.error(error);
    });

この例では、sharedResourceの最終的な値は、asyncTask1asyncTask2のどちらが先に完了するかによって変わります。これが競争条件の典型的な例です。

Promise.raceでの競争条件の処理

Promise.raceは、競争条件を効果的に管理するための手段を提供します。特定の条件が満たされた時点で次の処理を進めたい場合や、最初に完了した操作に基づいて後続の処理を行いたい場合に便利です。

以下の例では、2つのネットワークリクエストを同時に実行し、最初に完了したリクエストの結果を採用します。

const fetchFromServer1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Server 1 response'), 500);
});

const fetchFromServer2 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Server 2 response'), 300);
});

Promise.race([fetchFromServer1, fetchFromServer2])
    .then(result => {
        console.log(result); // "Server 2 response"
    })
    .catch(error => {
        console.error(error);
    });

この例では、fetchFromServer2が先に完了するため、その結果が採用されます。Promise.raceを使用することで、競争条件を適切に処理し、最初に応答したリクエストを迅速に処理することができます。

競争条件の回避

競争条件を避けるためには、以下のポイントに注意することが重要です:

  • リソースの排他制御を行う
  • 非同期処理の完了順序を意識する
  • 適切なエラーハンドリングを実装する

Promise.raceは、これらのポイントを押さえつつ、複数の非同期操作のうち最初に完了したものに基づいて効率的に処理を進めるための強力なツールです。

エラーハンドリング

Promise.raceを使用する際のエラーハンドリングは非常に重要です。複数の非同期操作の中で最初にエラーが発生した場合、そのエラーがPromise.raceの結果として返されます。これにより、エラーが早期に検出され、適切な対処が可能になります。

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

以下の例では、2つの非同期操作のうち、どちらかがエラーをスローした場合に、そのエラーをキャッチして処理します。

const task1 = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Task 1 failed')), 300);
});

const task2 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Task 2 completed'), 500);
});

Promise.race([task1, task2])
    .then(result => {
        console.log(result); // This will not be called in this example
    })
    .catch(error => {
        console.error(error.message); // "Task 1 failed"
    });

この例では、task1が300ミリ秒後にエラーをスローするため、Promise.raceはそのエラーをキャッチし、エラーメッセージ”Task 1 failed”をコンソールに出力します。

ネットワークリクエストのエラーハンドリング

Promise.raceを使用して複数のネットワークリクエストを同時に実行する場合、エラーハンドリングも考慮する必要があります。以下の例では、2つのリクエストのうちどちらかが失敗した場合にエラーを処理します。

const fetchFromServer1 = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Server 1 request failed')), 500);
});

const fetchFromServer2 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Server 2 response'), 300);
});

Promise.race([fetchFromServer1, fetchFromServer2])
    .then(result => {
        console.log(result); // "Server 2 response"
    })
    .catch(error => {
        console.error(error.message); // This will not be called in this example
    });

この例では、fetchFromServer2が先に成功するため、その結果が出力され、fetchFromServer1のエラーは無視されます。しかし、もしfetchFromServer2がエラーをスローした場合、そのエラーがキャッチされて処理されます。

タイムアウト処理のエラーハンドリング

タイムアウト処理と組み合わせることで、非同期操作が一定時間内に完了しない場合のエラー処理も可能です。

const fetchData = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Data fetched successfully'), 1000);
});

const timeout = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Request timed out')), 500);
});

Promise.race([fetchData, timeout])
    .then(result => {
        console.log(result); // This will not be called in this example
    })
    .catch(error => {
        console.error(error.message); // "Request timed out"
    });

この例では、timeoutが500ミリ秒後にエラーをスローし、fetchDataが1000ミリ秒後に成功します。Promise.raceは最初に発生したtimeoutのエラーをキャッチし、”Request timed out”と表示されます。

エラーハンドリングを適切に実装することで、Promise.raceを使った非同期処理がより堅牢になり、エラー発生時のトラブルシューティングが容易になります。

タイムアウト処理

タイムアウト処理は、Promise.raceを使用する際の重要なユースケースの一つです。特定の非同期操作が一定時間内に完了しない場合に、エラーをスローして処理を中断することで、アプリケーションのレスポンスを保つことができます。

タイムアウト処理の実装

以下の例では、Promise.raceを使用して、ネットワークリクエストが一定時間内に完了しない場合にタイムアウトエラーをスローする方法を紹介します。

const fetchData = new Promise((resolve, reject) => {
    setTimeout(() => resolve('Data fetched successfully'), 1000);
});

const timeout = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Request timed out')), 500);
});

Promise.race([fetchData, timeout])
    .then(result => {
        console.log(result); // This will not be called in this example
    })
    .catch(error => {
        console.error(error.message); // "Request timed out"
    });

この例では、fetchDataが1000ミリ秒で完了し、timeoutは500ミリ秒でエラーをスローします。Promise.raceは最初に完了したtimeoutのエラーをキャッチし、”Request timed out”と表示されます。

タイムアウト処理の応用

実際のアプリケーションでは、タイムアウト処理はネットワークリクエストだけでなく、ユーザーインターフェイスの応答性向上やバックエンドとの通信などにも使用されます。

例1:ユーザーインターフェイスの応答性向上

ユーザーがボタンをクリックしてデータを取得する場合、一定時間内にレスポンスが得られないときにエラーメッセージを表示する例です。

const button = document.getElementById('fetchButton');
const resultContainer = document.getElementById('result');

button.addEventListener('click', () => {
    const fetchData = new Promise((resolve, reject) => {
        setTimeout(() => resolve('Data fetched successfully'), 2000); // Simulating a long network request
    });

    const timeout = new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error('Operation timed out')), 1000);
    });

    Promise.race([fetchData, timeout])
        .then(result => {
            resultContainer.textContent = result;
        })
        .catch(error => {
            resultContainer.textContent = error.message; // "Operation timed out"
        });
});

この例では、ボタンをクリックすると、2秒後にデータが取得されますが、1秒以内に完了しない場合はタイムアウトエラーが表示されます。

例2:バックエンドとの通信のタイムアウト

以下の例では、バックエンドのAPIからデータを取得する際に、一定時間内にレスポンスが得られない場合にタイムアウトエラーを処理します。

async function fetchDataWithTimeout(url, timeoutMs) {
    const fetchPromise = fetch(url).then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.json();
    });

    const timeoutPromise = new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error('Request timed out')), timeoutMs);
    });

    try {
        const result = await Promise.race([fetchPromise, timeoutPromise]);
        return result;
    } catch (error) {
        throw error;
    }
}

// Usage example
fetchDataWithTimeout('https://api.example.com/data', 3000)
    .then(data => {
        console.log('Data:', data);
    })
    .catch(error => {
        console.error('Error:', error.message); // "Request timed out" if the request takes longer than 3 seconds
    });

この例では、fetchDataWithTimeout関数を使用して、バックエンドAPIからデータを取得し、3秒以内にレスポンスが得られない場合にタイムアウトエラーを処理します。

タイムアウト処理を適切に実装することで、アプリケーションの応答性を維持し、ユーザーエクスペリエンスを向上させることができます。Promise.raceはこの目的において非常に有効なツールです。

ネットワークリクエストの競争条件

ネットワークリクエストの処理において、複数のリクエストを同時に実行し、最初に完了したリクエストの結果を使用する場合があります。Promise.raceを使用すると、こうしたシナリオで効果的に競争条件を処理することができます。

競争条件の具体例

以下の例では、2つのAPIエンドポイントに対して同時にリクエストを送信し、最初に応答が得られたリクエストの結果を使用します。

const request1 = fetch('https://api.example.com/data1')
    .then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok for request1');
        }
        return response.json();
    });

const request2 = fetch('https://api.example.com/data2')
    .then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok for request2');
        }
        return response.json();
    });

Promise.race([request1, request2])
    .then(result => {
        console.log('First response received:', result);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

この例では、request1request2が同時に実行され、最初に完了したリクエストの結果がコンソールに出力されます。もう一方のリクエストがエラーになっても、最初に完了したリクエストが成功すれば、その結果が使用されます。

レスポンスタイムの競争条件

複数のサーバーに同時にリクエストを送り、最も速く応答するサーバーの結果を使用するシナリオを考えます。

const server1 = fetch('https://server1.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error('Server 1 failed to respond');
        }
        return response.json();
    });

const server2 = fetch('https://server2.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error('Server 2 failed to respond');
        }
        return response.json();
    });

Promise.race([server1, server2])
    .then(result => {
        console.log('Fastest server response:', result);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

この例では、server1server2のどちらかが最初に応答した場合、その結果がPromise.raceの結果として使用されます。応答速度が最も速いサーバーの結果を優先することで、ユーザー体験を向上させることができます。

冗長性とフォールバックの実装

ネットワークリクエストの冗長性とフォールバックを実装するためにもPromise.raceは有用です。例えば、プライマリサーバーがダウンしている場合にセカンダリサーバーにフォールバックすることができます。

const primaryServer = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Primary server is down')), 200);
});

const secondaryServer = fetch('https://secondary.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error('Secondary server failed to respond');
        }
        return response.json();
    });

Promise.race([primaryServer, secondaryServer])
    .then(result => {
        console.log('Data from server:', result);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

この例では、primaryServerが200ミリ秒後に失敗し、secondaryServerが応答するので、フォールバックが自動的に行われます。これにより、サーバーダウン時にもサービスを継続できるようになります。

ネットワークリクエストの競争条件をPromise.raceで処理することで、より信頼性が高く、応答性の良いアプリケーションを実現することができます。

ユーザーインターフェイスでの応用

Promise.raceは、ユーザーインターフェイス(UI)の応答性を向上させるためにも有効です。特に、ユーザーの操作に対する応答を迅速に行う必要がある場合に役立ちます。ここでは、UIでPromise.raceを使用する具体的なケーススタディを紹介します。

例1:複数のリソースの読み込み

ユーザーがページを開いた際に、複数の画像リソースを同時に読み込み、最初に完了したリソースを表示する例です。

const loadImage = (url) => {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = () => reject(new Error(`Failed to load image from ${url}`));
        img.src = url;
    });
};

const image1 = loadImage('https://example.com/image1.jpg');
const image2 = loadImage('https://example.com/image2.jpg');

Promise.race([image1, image2])
    .then(img => {
        document.body.appendChild(img);
    })
    .catch(error => {
        console.error(error.message);
    });

この例では、loadImage関数を使って2つの画像を同時に読み込み、最初に完了した画像をページに表示します。これにより、ユーザーはより早くコンテンツを閲覧できるようになります。

例2:ユーザー入力のタイムアウト処理

ユーザーが入力を行う際に、一定時間内に入力が完了しない場合にエラーメッセージを表示する例です。

const userInputPromise = new Promise((resolve, reject) => {
    const inputField = document.getElementById('userInput');
    inputField.addEventListener('input', () => resolve(inputField.value));
});

const timeoutPromise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Input timed out')), 5000);
});

Promise.race([userInputPromise, timeoutPromise])
    .then(value => {
        console.log('User input:', value);
    })
    .catch(error => {
        console.error(error.message); // "Input timed out"
    });

この例では、ユーザーが入力フィールドにテキストを入力しない場合、5秒後にタイムアウトエラーが発生します。これにより、ユーザーの入力が一定時間内に行われない場合に適切なフィードバックを提供できます。

例3:APIリクエストの応答性向上

ユーザーがボタンをクリックしてデータを取得する際に、複数のAPIエンドポイントにリクエストを送り、最初に応答が得られたデータを使用する例です。

const button = document.getElementById('fetchButton');
const resultContainer = document.getElementById('result');

button.addEventListener('click', () => {
    const request1 = fetch('https://api1.example.com/data')
        .then(response => response.json());

    const request2 = fetch('https://api2.example.com/data')
        .then(response => response.json());

    Promise.race([request1, request2])
        .then(data => {
            resultContainer.textContent = JSON.stringify(data);
        })
        .catch(error => {
            resultContainer.textContent = error.message;
        });
});

この例では、ボタンをクリックすると、2つのAPIエンドポイントに対して同時にリクエストが送信されます。最初に応答したAPIのデータが表示され、ユーザーはより早く結果を見ることができます。

Promise.raceをUIに適用することで、ユーザー体験を向上させ、迅速なフィードバックを提供することができます。特に、複数の非同期操作が関与する場合に、その効果は顕著です。

他のPromiseメソッドとの比較

JavaScriptには、Promise.raceの他にも複数のPromiseメソッドがあり、それぞれ異なる用途に適しています。ここでは、Promise.raceと、Promise.all、Promise.allSettled、Promise.anyとの違いと使い分けについて解説します。

Promise.all

Promise.allは、渡されたすべてのPromiseが解決されるまで待ちます。すべてのPromiseが成功した場合、その結果を配列で返します。いずれかのPromiseが拒否された場合、即座にエラーを返します。

const promise1 = Promise.resolve('Promise 1 resolved');
const promise2 = Promise.resolve('Promise 2 resolved');

Promise.all([promise1, promise2])
    .then(results => {
        console.log(results); // ["Promise 1 resolved", "Promise 2 resolved"]
    })
    .catch(error => {
        console.error(error);
    });

使用シナリオ

Promise.allは、すべての非同期操作が完了するまで待ち、その結果を一度に取得したい場合に適しています。例えば、複数のAPIリクエストの結果をまとめて処理する場合です。

Promise.allSettled

Promise.allSettledは、渡されたすべてのPromiseが完了(解決または拒否)されるまで待ちます。各Promiseの結果はオブジェクトとして返され、そのオブジェクトにはstatusプロパティ(”fulfilled”または”rejected”)が含まれます。

const promise1 = Promise.resolve('Promise 1 resolved');
const promise2 = Promise.reject('Promise 2 rejected');

Promise.allSettled([promise1, promise2])
    .then(results => {
        console.log(results);
        // [{status: "fulfilled", value: "Promise 1 resolved"}, {status: "rejected", reason: "Promise 2 rejected"}]
    });

使用シナリオ

Promise.allSettledは、すべてのPromiseの完了を待ち、成功と失敗の両方の結果を処理したい場合に適しています。例えば、複数のAPIリクエストの結果を個別に処理する必要がある場合です。

Promise.any

Promise.anyは、渡されたPromiseのうち、最初に解決されたものの結果を返します。すべてのPromiseが拒否された場合、エラーを返します。

const promise1 = Promise.reject('Promise 1 rejected');
const promise2 = Promise.resolve('Promise 2 resolved');

Promise.any([promise1, promise2])
    .then(result => {
        console.log(result); // "Promise 2 resolved"
    })
    .catch(error => {
        console.error(error);
    });

使用シナリオ

Promise.anyは、複数の非同期操作のうち、最初に成功したものの結果を取得したい場合に適しています。例えば、複数のキャッシュサーバーから最初に応答を得たものを使用する場合です。

Promise.race

Promise.raceは、渡されたPromiseのうち、最初に完了(解決または拒否)したものの結果を返します。

const promise1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'Promise 1 resolved');
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(reject, 300, 'Promise 2 rejected');
});

Promise.race([promise1, promise2])
    .then(result => {
        console.log(result); // "Promise 2 rejected"
    })
    .catch(error => {
        console.error(error); // "Promise 2 rejected"
    });

使用シナリオ

Promise.raceは、複数の非同期操作のうち、最初に完了したものに基づいて次の処理を進めたい場合に適しています。例えば、最初に応答したサーバーの結果を使用する場合や、一定時間内に完了しない操作をタイムアウトさせる場合です。

まとめ

各Promiseメソッドには特定の用途があり、シナリオに応じて使い分けることが重要です。Promise.allはすべての操作が成功するまで待ちたい場合に、Promise.allSettledは成功と失敗を含むすべての結果を処理したい場合に、Promise.anyは最初に成功した操作の結果を取得したい場合に、そしてPromise.raceは最初に完了した操作の結果を優先したい場合にそれぞれ適しています。これらのメソッドを適切に使用することで、非同期処理を効率的に管理することができます。

パフォーマンスの考慮

Promise.raceを使用する際には、パフォーマンスに関するいくつかの考慮点があります。非同期処理の効率を最大限に引き出すためには、これらのポイントに注意することが重要です。

不要なリクエストの削減

Promise.raceは最初に完了したPromiseの結果を使用するため、その他のPromiseは完了しても結果が無視されます。そのため、不要なリクエストを避けるための工夫が必要です。

const cancelableFetch = (url) => {
    let hasCanceled = false;

    const fetchPromise = fetch(url).then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.json();
    });

    return {
        promise: new Promise((resolve, reject) => {
            fetchPromise
                .then(result => !hasCanceled && resolve(result))
                .catch(error => !hasCanceled && reject(error));
        }),
        cancel() {
            hasCanceled = true;
        }
    };
};

const request1 = cancelableFetch('https://api.example.com/data1');
const request2 = cancelableFetch('https://api.example.com/data2');

Promise.race([request1.promise, request2.promise])
    .then(result => {
        console.log('First response:', result);
        request1.cancel();
        request2.cancel();
    })
    .catch(error => {
        console.error('Error:', error.message);
        request1.cancel();
        request2.cancel();
    });

この例では、cancelableFetch関数を使用してキャンセル可能なリクエストを作成しています。Promise.raceが最初の結果を取得した後、他のリクエストをキャンセルすることで不要なリクエストの処理を削減します。

並行処理の適切な使用

Promise.raceは複数のPromiseを並行して実行するため、並行処理が適切に行われているか確認する必要があります。過剰な並行処理はリソースの無駄遣いにつながるため、必要な範囲内で使用することが重要です。

例:画像のプリロード

多数の画像を一度にプリロードする場合、リソースの消費が問題となることがあります。適切に制御された並行処理を行うことで、リソースを効率的に使用できます。

const preloadImage = (url) => {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(url);
        img.onerror = () => reject(new Error(`Failed to load image from ${url}`));
        img.src = url;
    });
};

const imageUrls = [
    'https://example.com/image1.jpg',
    'https://example.com/image2.jpg',
    // More URLs...
];

const preloadImages = (urls) => {
    const promises = urls.map(url => preloadImage(url));
    return Promise.all(promises);
};

preloadImages(imageUrls)
    .then(results => {
        console.log('All images preloaded:', results);
    })
    .catch(error => {
        console.error('Error preloading images:', error.message);
    });

この例では、Promise.allを使用してすべての画像を並行してプリロードしていますが、これにより一度に大量のリクエストが送信されます。必要に応じて並行処理の数を制限することが考慮されるべきです。

メモリとリソースの管理

大量のPromiseを扱う際には、メモリとリソースの管理も重要です。長時間実行される非同期操作や、結果が大きい場合には特に注意が必要です。

const fetchData = (url) => {
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        });
};

const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    // More URLs...
];

const fetchAllData = async (urls) => {
    const results = [];
    for (const url of urls) {
        const data = await fetchData(url);
        results.push(data);
    }
    return results;
};

fetchAllData(urls)
    .then(results => {
        console.log('All data fetched:', results);
    })
    .catch(error => {
        console.error('Error fetching data:', error.message);
    });

この例では、forループを使用して順次非同期操作を実行することで、同時に発生するリクエストの数を制御しています。

適切なエラーハンドリング

Promise.raceを使用する際には、適切なエラーハンドリングも重要です。エラーが発生した場合に迅速に対処することで、パフォーマンスへの影響を最小限に抑えることができます。

const fetchDataWithTimeout = (url, timeoutMs) => {
    const fetchPromise = fetch(url).then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.json();
    });

    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Request timed out')), timeoutMs);
    });

    return Promise.race([fetchPromise, timeoutPromise]);
};

fetchDataWithTimeout('https://api.example.com/data', 3000)
    .then(data => {
        console.log('Data:', data);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

この例では、一定時間内にリクエストが完了しない場合にタイムアウトエラーを発生させ、適切に処理します。

適切なパフォーマンスの考慮とエラーハンドリングを行うことで、Promise.raceを効率的かつ効果的に使用できるようになります。これにより、非同期処理の信頼性と応答性が向上し、アプリケーションのパフォーマンスが全体的に改善されます。

演習問題

Promise.raceの理解を深めるために、いくつかの演習問題を用意しました。これらの問題に取り組むことで、実際のコードを書きながら学ぶことができます。

問題1:画像読み込みの競争

3つの画像URLから最初に読み込まれた画像を表示するコードを書いてください。画像の読み込みが失敗した場合は、エラーメッセージを表示してください。

const loadImage = (url) => {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = () => reject(new Error(`Failed to load image from ${url}`));
        img.src = url;
    });
};

const imageUrls = [
    'https://example.com/image1.jpg',
    'https://example.com/image2.jpg',
    'https://example.com/image3.jpg'
];

Promise.race(imageUrls.map(loadImage))
    .then(img => {
        document.body.appendChild(img);
    })
    .catch(error => {
        console.error(error.message);
    });

問題2:APIリクエストのタイムアウト

APIリクエストが3秒以内に完了しない場合にタイムアウトエラーをスローするコードを書いてください。

const fetchDataWithTimeout = (url, timeoutMs) => {
    const fetchPromise = fetch(url).then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.json();
    });

    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Request timed out')), timeoutMs);
    });

    return Promise.race([fetchPromise, timeoutPromise]);
};

fetchDataWithTimeout('https://api.example.com/data', 3000)
    .then(data => {
        console.log('Data:', data);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

問題3:ボタンのクリック待機とタイムアウト

ユーザーがボタンをクリックするか、5秒以内にクリックしない場合にタイムアウトエラーを表示するコードを書いてください。

const button = document.getElementById('clickButton');

const buttonClickPromise = new Promise((resolve) => {
    button.addEventListener('click', () => resolve('Button clicked'));
});

const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Button click timed out')), 5000);
});

Promise.race([buttonClickPromise, timeoutPromise])
    .then(message => {
        console.log(message); // "Button clicked"
    })
    .catch(error => {
        console.error(error.message); // "Button click timed out"
    });

問題4:複数のAPIから最初の成功応答を取得

複数のAPIエンドポイントに対してリクエストを送り、最初に成功したレスポンスのデータを取得するコードを書いてください。

const fetchFromApi1 = fetch('https://api1.example.com/data').then(response => {
    if (!response.ok) {
        throw new Error('API 1 response was not ok');
    }
    return response.json();
});

const fetchFromApi2 = fetch('https://api2.example.com/data').then(response => {
    if (!response.ok) {
        throw new Error('API 2 response was not ok');
    }
    return response.json();
});

const fetchFromApi3 = fetch('https://api3.example.com/data').then(response => {
    if (!response.ok) {
        throw new Error('API 3 response was not ok');
    }
    return response.json();
});

Promise.race([fetchFromApi1, fetchFromApi2, fetchFromApi3])
    .then(data => {
        console.log('First successful response:', data);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

問題5:ユーザー入力の最初の完了を取得

複数の入力フィールドから最初に入力が完了したフィールドの値を取得するコードを書いてください。

const input1 = document.getElementById('input1');
const input2 = document.getElementById('input2');
const input3 = document.getElementById('input3');

const inputPromise = (input) => {
    return new Promise((resolve) => {
        input.addEventListener('input', () => resolve(input.value));
    });
};

Promise.race([inputPromise(input1), inputPromise(input2), inputPromise(input3)])
    .then(value => {
        console.log('First input completed with value:', value);
    });

これらの演習問題に取り組むことで、Promise.raceの使用方法とその効果的な応用についての理解を深めることができます。各問題を解く際には、エラーハンドリングやリソース管理にも注意を払いながら実装してみてください。

まとめ

本記事では、JavaScriptのPromise.raceを利用した競争条件の処理方法について詳しく解説しました。Promise.raceの基本概念から、具体的な使用例、エラーハンドリング、タイムアウト処理、ネットワークリクエストの応用、ユーザーインターフェイスでの実装方法、他のPromiseメソッドとの比較、パフォーマンスの考慮点まで幅広く取り上げました。最後に、実践的な演習問題を通じて、Promise.raceの効果的な使用方法を学びました。

Promise.raceは、非同期操作の中で最初に完了したものを取得する際に非常に有用なツールです。特に、タイムアウト処理や複数のネットワークリクエストの競争条件を効率的に管理するために役立ちます。適切なエラーハンドリングとパフォーマンス管理を行うことで、アプリケーションの信頼性と応答性を向上させることができます。

これらの知識を活用して、より効率的で堅牢な非同期処理を実現し、ユーザー体験を向上させるプロジェクトを開発してください。

コメント

コメントする

目次