Beheers JavaScript Promise combinators (Promise.all, Promise.allSettled, Promise.race, Promise.any) voor efficiënte en robuuste asynchrone programmering in wereldwijde applicaties.
JavaScript Promise Combinators: Geavanceerde Async Patronen voor Wereldwijde Applicaties
Asynchroon programmeren is een hoeksteen van modern JavaScript, vooral bij het bouwen van webapplicaties die communiceren met API's, databases of tijdrovende operaties uitvoeren. JavaScript Promises bieden een krachtige abstractie voor het beheren van asynchrone operaties, maar om ze te beheersen is inzicht in geavanceerde patronen vereist. Dit artikel duikt in JavaScript Promise combinators – Promise.all, Promise.allSettled, Promise.race, en Promise.any – en hoe deze kunnen worden gebruikt om efficiënte en robuuste asynchrone workflows te creëren, met name in de context van wereldwijde applicaties met variërende netwerkomstandigheden en databronnen.
Promises Begrijpen: Een Snelle Samenvatting
Voordat we in de combinators duiken, laten we eerst Promises kort herhalen. Een Promise vertegenwoordigt het uiteindelijke resultaat van een asynchrone operatie. Het kan zich in een van de volgende drie staten bevinden:
- Pending: De initiële staat, noch vervuld, noch afgewezen.
- Fulfilled: De operatie is succesvol voltooid, met een resulterende waarde.
- Rejected: De operatie is mislukt, met een reden (meestal een Error-object).
Promises bieden een schonere en beter beheersbare manier om met asynchrone operaties om te gaan in vergelijking met traditionele callbacks. Ze verbeteren de leesbaarheid van de code en vereenvoudigen de foutafhandeling. Cruciaal is dat ze ook de basis vormen voor de Promise combinators die we zullen verkennen.
Promise Combinators: Asynchrone Operaties Orkestreren
Promise combinators zijn statische methoden op het Promise-object waarmee je meerdere Promises kunt beheren en coördineren. Ze bieden krachtige tools voor het bouwen van complexe asynchrone workflows. Laten we elk van hen in detail bekijken.
Promise.all(): Promises Parallel Uitvoeren en Resultaten Aggregeren
Promise.all() neemt een iterable (meestal een array) van Promises als input en retourneert een enkele Promise. Deze geretourneerde Promise wordt vervuld wanneer alle input Promises zijn vervuld. Als een van de input Promises wordt afgewezen, wordt de geretourneerde Promise onmiddellijk afgewezen met de reden van de eerste afgewezen Promise.
Gebruiksscenario: Wanneer je gelijktijdig data van meerdere API's moet ophalen en de gecombineerde resultaten moet verwerken, is Promise.all() ideaal. Stel je bijvoorbeeld voor dat je een dashboard bouwt dat weersinformatie van verschillende steden over de hele wereld weergeeft. De data van elke stad kan via een afzonderlijke API-aanroep worden opgehaald.
async function fetchWeatherData(city) {
try {
const response = await fetch(`https://api.example.com/weather?city=${city}`); // Vervang door een echt API-eindpunt
if (!response.ok) {
throw new Error(`Failed to fetch weather data for ${city}`);
}
return await response.json();
} catch (error) {
console.error(`Error fetching weather data for ${city}: ${error}`);
throw error; // Gooi de fout opnieuw op zodat deze door Promise.all kan worden opgevangen
}
}
async function displayWeatherData() {
const cities = ['London', 'Tokyo', 'New York', 'Sydney'];
try {
const weatherDataPromises = cities.map(city => fetchWeatherData(city));
const weatherData = await Promise.all(weatherDataPromises);
weatherData.forEach((data, index) => {
console.log(`Weather in ${cities[index]}:`, data);
// Werk de UI bij met de weerdata
});
} catch (error) {
console.error('Failed to fetch weather data for all cities:', error);
// Toon een foutmelding aan de gebruiker
}
}
displayWeatherData();
Overwegingen voor Wereldwijde Applicaties:
- Netwerklatentie: Aanvragen naar verschillende API's op verschillende geografische locaties kunnen variërende latentie ervaren.
Promise.all()garandeert niet de volgorde waarin de Promises worden vervuld, alleen dat ze allemaal worden vervuld (of één wordt afgewezen) voordat de gecombineerde Promise is afgehandeld. - API Rate Limiting: Als je meerdere aanvragen doet naar dezelfde API of meerdere API's met gedeelde rate limits, kun je deze limieten overschrijden. Implementeer strategieën zoals het in de wachtrij plaatsen van aanvragen of het gebruik van 'exponential backoff' om rate limiting soepel af te handelen.
- Foutafhandeling: Onthoud dat als ook maar één Promise wordt afgewezen, de hele
Promise.all()-operatie mislukt. Dit is misschien niet wenselijk als je gedeeltelijke data wilt weergeven, zelfs als sommige aanvragen mislukken. Overweeg in dergelijke gevallenPromise.allSettled()te gebruiken (hieronder uitgelegd).
Promise.allSettled(): Succes en Mislukking Individueel Afhandelen
Promise.allSettled() is vergelijkbaar met Promise.all(), maar met een cruciaal verschil: het wacht tot alle input Promises zijn afgehandeld ('settled'), ongeacht of ze worden vervuld of afgewezen. De geretourneerde Promise wordt altijd vervuld met een array van objecten, die elk de uitkomst van de corresponderende input Promise beschrijven. Elk object heeft een status-eigenschap (ofwel "fulfilled" of "rejected") en een value- (indien vervuld) of reason- (indien afgewezen) eigenschap.
Gebruiksscenario: Wanneer je resultaten van meerdere asynchrone operaties moet verzamelen, en het acceptabel is dat sommige mislukken zonder de hele operatie te laten falen, is Promise.allSettled() de betere keuze. Stel je een systeem voor dat betalingen verwerkt via meerdere betalingsgateways. Je wilt misschien alle betalingen proberen en registreren welke zijn geslaagd en welke zijn mislukt.
async function processPayment(paymentGateway, amount) {
try {
const response = await paymentGateway.process(amount); // Vervang door een echte betalingsgateway-integratie
if (response.status === 'success') {
return { status: 'fulfilled', value: `Payment processed successfully via ${paymentGateway.name}` };
} else {
throw new Error(`Payment failed via ${paymentGateway.name}: ${response.message}`);
}
} catch (error) {
return { status: 'rejected', reason: `Payment failed via ${paymentGateway.name}: ${error.message}` };
}
}
async function processMultiplePayments(paymentGateways, amount) {
const paymentPromises = paymentGateways.map(gateway => processPayment(gateway, amount));
const results = await Promise.allSettled(paymentPromises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
// Analyseer de resultaten om het algehele succes/falen te bepalen
const successfulPayments = results.filter(result => result.status === 'fulfilled').length;
const failedPayments = results.filter(result => result.status === 'rejected').length;
console.log(`Successful payments: ${successfulPayments}`);
console.log(`Failed payments: ${failedPayments}`);
}
// Voorbeeld betalingsgateways
const paymentGateways = [
{ name: 'PayPal', process: (amount) => Promise.resolve({ status: 'success', message: 'Payment successful' }) },
{ name: 'Stripe', process: (amount) => Promise.reject({ status: 'error', message: 'Insufficient funds' }) },
{ name: 'Worldpay', process: (amount) => Promise.resolve({ status: 'success', message: 'Payment successful' }) },
];
processMultiplePayments(paymentGateways, 100);
Overwegingen voor Wereldwijde Applicaties:
- Robuustheid:
Promise.allSettled()verhoogt de robuustheid van je applicaties door ervoor te zorgen dat alle asynchrone operaties worden geprobeerd, zelfs als sommige mislukken. Dit is met name belangrijk in gedistribueerde systemen waar storingen vaak voorkomen. - Gedetailleerde Rapportage: De resultatenarray biedt gedetailleerde informatie over de uitkomst van elke operatie, waardoor je fouten kunt loggen, mislukte operaties opnieuw kunt proberen of gebruikers specifieke feedback kunt geven.
- Gedeeltelijk Succes: Je kunt eenvoudig het algehele slagingspercentage bepalen en passende acties ondernemen op basis van het aantal succesvolle en mislukte operaties. Je kunt bijvoorbeeld alternatieve betaalmethoden aanbieden als de primaire gateway faalt.
Promise.race(): Het Snelste Resultaat Kiezen
Promise.race() neemt ook een iterable van Promises als input en retourneert een enkele Promise. Echter, in tegenstelling tot Promise.all() en Promise.allSettled(), wordt Promise.race() afgehandeld zodra één van de input Promises wordt afgehandeld (ofwel vervuld of afgewezen). De geretourneerde Promise wordt vervuld of afgewezen met de waarde of reden van de eerste afgehandelde Promise.
Gebruiksscenario: Wanneer je de snelste reactie uit meerdere bronnen moet selecteren, is Promise.race() een goede keuze. Stel je voor dat je meerdere servers bevraagt voor dezelfde data en de eerste reactie gebruikt die je ontvangt. Dit kan de prestaties en responsiviteit verbeteren, vooral in situaties waarin sommige servers tijdelijk niet beschikbaar of langzamer zijn dan andere.
async function fetchDataFromServer(serverURL) {
try {
const response = await fetch(serverURL, {signal: AbortSignal.timeout(5000)}); //Voeg een time-out van 5 seconden toe
if (!response.ok) {
throw new Error(`Failed to fetch data from ${serverURL}`);
}
return await response.json();
} catch (error) {
console.error(`Error fetching data from ${serverURL}: ${error}`);
throw error;
}
}
async function getFastestResponse() {
const serverURLs = [
'https://server1.example.com/data', // Vervang door echte server-URL's
'https://server2.example.com/data',
'https://server3.example.com/data',
];
try {
const dataPromises = serverURLs.map(serverURL => fetchDataFromServer(serverURL));
const fastestData = await Promise.race(dataPromises);
console.log('Fastest data received:', fastestData);
// Gebruik de snelste data
} catch (error) {
console.error('Failed to get data from any server:', error);
// Handel de fout af
}
}
getFastestResponse();
Overwegingen voor Wereldwijde Applicaties:
- Time-outs: Het is cruciaal om time-outs te implementeren bij het gebruik van
Promise.race()om te voorkomen dat de geretourneerde Promise oneindig wacht als sommige van de input Promises nooit worden afgehandeld. Het bovenstaande voorbeeld gebruikt `AbortSignal.timeout()` om dit te bereiken. - Netwerkomstandigheden: De snelste server kan variëren afhankelijk van de geografische locatie en netwerkomstandigheden van de gebruiker. Overweeg het gebruik van een Content Delivery Network (CDN) om je content te distribueren en de prestaties voor gebruikers over de hele wereld te verbeteren.
- Foutafhandeling: Als de Promise die de race 'wint' wordt afgewezen, dan wordt de hele Promise.race afgewezen. Zorg ervoor dat elke Promise de juiste foutafhandeling heeft om onverwachte afwijzingen te voorkomen. Ook als de "winnende" promise wordt afgewezen vanwege een time-out (zoals hierboven getoond), zullen de andere promises op de achtergrond blijven uitvoeren. Mogelijk moet je logica toevoegen om die andere promises te annuleren met `AbortController` als ze niet langer nodig zijn.
Promise.any(): De Eerste Vervulling Accepteren
Promise.any() is vergelijkbaar met Promise.race(), maar met een iets ander gedrag. Het wacht op de eerste input Promise die wordt vervuld. Als alle input Promises worden afgewezen, wordt Promise.any() afgewezen met een AggregateError die een array van de afwijzingsredenen bevat.
Gebruiksscenario: Wanneer je data uit meerdere bronnen moet ophalen en je alleen geïnteresseerd bent in het eerste succesvolle resultaat, is Promise.any() een goede keuze. Dit is handig wanneer je redundante databronnen of alternatieve API's hebt die dezelfde informatie leveren. Het geeft prioriteit aan succes boven snelheid, omdat het wacht op de eerste vervulling, zelfs als sommige Promises snel worden afgewezen.
async function fetchDataFromSource(sourceURL) {
try {
const response = await fetch(sourceURL);
if (!response.ok) {
throw new Error(`Failed to fetch data from ${sourceURL}`);
}
return await response.json();
} catch (error) {
console.error(`Error fetching data from ${sourceURL}: ${error}`);
throw error;
}
}
async function getFirstSuccessfulData() {
const dataSources = [
'https://source1.example.com/data', // Vervang door echte databron-URL's
'https://source2.example.com/data',
'https://source3.example.com/data',
];
try {
const dataPromises = dataSources.map(sourceURL => fetchDataFromSource(sourceURL));
const data = await Promise.any(dataPromises);
console.log('First successful data received:', data);
// Gebruik de succesvolle data
} catch (error) {
if (error instanceof AggregateError) {
console.error('Failed to get data from any source:', error.errors);
// Handel de fout af
} else {
console.error('An unexpected error occurred:', error);
}
}
}
getFirstSuccessfulData();
Overwegingen voor Wereldwijde Applicaties:
- Redundantie:
Promise.any()is bijzonder nuttig bij het omgaan met redundante databronnen die vergelijkbare informatie leveren. Als één bron niet beschikbaar of traag is, kun je op de andere vertrouwen om de data te leveren. - Foutafhandeling: Zorg ervoor dat je de
AggregateErrorafhandelt die wordt geworpen wanneer alle input Promises worden afgewezen. Deze fout bevat een array van de individuele afwijzingsredenen, waardoor je de problemen kunt debuggen en diagnosticeren. - Prioritering: De volgorde waarin je de Promises aan
Promise.any()doorgeeft, is van belang. Plaats de meest betrouwbare of snelste databronnen eerst om de kans op een succesvol resultaat te vergroten.
De Juiste Combinator Kiezen: Een Samenvatting
Hier is een snelle samenvatting om je te helpen de juiste Promise combinator voor jouw behoeften te kiezen:
- Promise.all(): Gebruik wanneer je wilt dat alle Promises succesvol worden vervuld, en je onmiddellijk wilt falen als een Promise wordt afgewezen.
- Promise.allSettled(): Gebruik wanneer je wilt wachten tot alle Promises zijn afgehandeld, ongeacht succes of mislukking, en je gedetailleerde informatie over elke uitkomst nodig hebt.
- Promise.race(): Gebruik wanneer je het snelste resultaat uit meerdere Promises wilt kiezen, en je alleen geïnteresseerd bent in de eerste die wordt afgehandeld.
- Promise.any(): Gebruik wanneer je het eerste succesvolle resultaat uit meerdere Promises wilt accepteren, en het je niet uitmaakt als sommige Promises worden afgewezen.
Geavanceerde Patronen en Best Practices
Naast het basisgebruik van Promise combinators, zijn er verschillende geavanceerde patronen en best practices om in gedachten te houden:
Concurrency Beperken
Bij het omgaan met een groot aantal Promises kan het parallel uitvoeren ervan je systeem overbelasten of API rate limits overschrijden. Je kunt concurrency beperken met technieken zoals:
- Chunking (Opdelen in stukken): Verdeel de Promises in kleinere stukken en verwerk elk stuk opeenvolgend.
- Een Semafoor Gebruiken: Implementeer een semafoor om het aantal gelijktijdige operaties te beheersen.
Hier is een voorbeeld met chunking:
async function processInChunks(promises, chunkSize) {
const results = [];
for (let i = 0; i < promises.length; i += chunkSize) {
const chunk = promises.slice(i, i + chunkSize);
const chunkResults = await Promise.all(chunk);
results.push(...chunkResults);
}
return results;
}
// Voorbeeldgebruik
const myPromises = [...Array(100)].map((_, i) => Promise.resolve(i)); //Maak 100 promises aan
processInChunks(myPromises, 10) // Verwerk 10 promises tegelijk
.then(results => console.log('All promises resolved:', results));
Fouten Gracieus Afhandelen
Goede foutafhandeling is cruciaal bij het werken met Promises. Gebruik try...catch-blokken om fouten op te vangen die kunnen optreden tijdens asynchrone operaties. Overweeg bibliotheken zoals p-retry of retry te gebruiken om mislukte operaties automatisch opnieuw te proberen.
async function fetchDataWithRetry(url, retries = 3) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (retries > 0) {
console.log(`Retrying in 1 second... (Retries left: ${retries})`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wacht 1 seconde
return fetchDataWithRetry(url, retries - 1);
} else {
console.error('Max retries reached. Operation failed.');
throw error;
}
}
}
Async/Await Gebruiken
async en await bieden een meer synchroon ogende manier om met Promises te werken. Ze kunnen de leesbaarheid en onderhoudbaarheid van de code aanzienlijk verbeteren.
Vergeet niet om try...catch-blokken rond await-expressies te gebruiken om mogelijke fouten af te handelen.
Annulering
In sommige scenario's moet je mogelijk lopende Promises annuleren, vooral bij langdurige operaties of door de gebruiker geïnitieerde acties. Je kunt de AbortController API gebruiken om aan te geven dat een Promise geannuleerd moet worden.
const controller = new AbortController();
const signal = controller.signal;
async function fetchDataWithCancellation(url) {
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Error fetching data:', error);
}
throw error;
}
}
fetchDataWithCancellation('https://api.example.com/data')
.then(data => console.log('Data received:', data))
.catch(error => console.error('Fetch failed:', error));
// Annuleer de fetch-operatie na 5 seconden
setTimeout(() => {
controller.abort();
}, 5000);
Conclusie
JavaScript Promise combinators zijn krachtige tools voor het bouwen van robuuste en efficiënte asynchrone applicaties. Door de nuances van Promise.all, Promise.allSettled, Promise.race en Promise.any te begrijpen, kun je complexe asynchrone workflows orkestreren, fouten gracieus afhandelen en prestaties optimaliseren. Bij het ontwikkelen van wereldwijde applicaties is het cruciaal om rekening te houden met netwerklatentie, API rate limits en de betrouwbaarheid van databronnen. Door de patronen en best practices die in dit artikel zijn besproken toe te passen, kun je JavaScript-applicaties creëren die zowel performant als veerkrachtig zijn, en een superieure gebruikerservaring bieden aan gebruikers over de hele wereld.