Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Promise combinators in JavaScript: .all, .allSettled, .any e .race in scenari reali

Scopriamo cosa sono, a cosa servono i Promise combinator in JavaScript e perché cambiano il modo di scrivere asincrono
Scopriamo cosa sono, a cosa servono i Promise combinator in JavaScript e perché cambiano il modo di scrivere asincrono
Link copiato negli appunti

Quando si lavora con codice asincrono in JavaScript, il vero problema non è solo "aspettare" un risultato, ma coordinare più operazioni contemporaneamente. Nel mondo reale le applicazioni raramente eseguono una sola chiamata alla volta. Si interrogano più API, si leggono più risorse, si avviano task paralleli che devono essere gestiti in modo coerente.

Perché servono davvero i combinatori di Promise

È qui che entrano in gioco i combinatori di Promise. Non sono semplici utility, ma strumenti fondamentali per esprimere logiche asincrone complesse in modo chiaro. .all, .allSettled, .any e .race rappresentano quattro strategie diverse per gestire gruppi di Promise. Capirle davvero significa poter scegliere il comportamento giusto in base al contesto.

La differenza tra queste funzioni non è solo tecnica. È semantica. Ognuna racconta una storia diversa su come vogliamo che il sistema reagisca quando più operazioni sono in corso.

Promise.all: quando tutto deve andare a buon fine

Il comportamento di Promise.all è probabilmente il più conosciuto, ma anche quello più spesso frainteso. L'idea è semplice: si avvia un insieme di Promise in parallelo e si aspetta che tutte si risolvano. Solo a quel punto si ottiene il risultato completo.

Se anche una sola Promise viene rifiutata, l'intera operazione fallisce immediatamente. Questo comportamento è perfetto quando tutte le operazioni sono indispensabili.

Immaginiamo una pagina che deve caricare i dati di un utente, i suoi ordini e le impostazioni:

async function caricaDashboard() {
  const [utente, ordini, impostazioni] = await Promise.all([
    fetch("/api/user").then(r => r.json()),
    fetch("/api/orders").then(r => r.json()),
    fetch("/api/settings").then(r => r.json())
  ]);

  return { utente, ordini, impostazioni };
}

Qui ha senso che tutto fallisca se una chiamata non va a buon fine. Una dashboard incompleta potrebbe essere inutilizzabile o incoerente.

Il vantaggio principale di .all è che le operazioni partono in parallelo, ma il risultato viene restituito in modo ordinato. Non si perde la corrispondenza tra input e output.

Quando .all diventa un problema
Il comportamento "fail fast" di Promise.all è potente, ma non sempre desiderabile. In alcuni scenari una singola Promise fallita non dovrebbe bloccare tutto.

Pensiamo a un sistema che carica contenuti opzionali, come widget o suggerimenti. Se uno di questi fallisce, non è necessario interrompere tutto il resto.

Se si usa .all in questi casi, si rischia di trasformare un problema minore in un errore globale.

Promise.allSettled: quando ogni risultato conta

Promise.allSettled nasce proprio per risolvere questo tipo di situazioni. Invece di fallire alla prima Promise rifiutata, aspetta che tutte le Promise siano completate, indipendentemente dal risultato.

Il valore restituito non è un array di risultati semplici, ma una struttura che indica per ogni Promise se è stata risolta o rifiutata.

Vediamo un esempio concreto:

async function caricaWidget() {
  const risultati = await Promise.allSettled([
    fetch("/api/news").then(r => r.json()),
    fetch("/api/weather").then(r => r.json()),
    fetch("/api/notifications").then(r => r.json())
  ]);

  return risultati.map(r => {
    if (r.status === "fulfilled") {
      return r.value;
    } else {
      return null;
    }
  });
}

In questo caso l'applicazione continua a funzionare anche se una delle chiamate fallisce. Ogni risultato viene gestito singolarmente.

Questo approccio è molto utile quando si lavora con contenuti opzionali o quando si vuole fornire la miglior esperienza possibile anche in condizioni non ideali.

.any: quando basta il primo successo

Promise.any introduce una logica completamente diversa. Qui non interessa che tutte le Promise vadano a buon fine, ma che almeno una riesca.

La prima Promise che si risolve determina il risultato finale. Se invece tutte falliscono, viene generato un errore.

Questo comportamento è perfetto quando si hanno più fonti alternative per ottenere lo stesso dato.

Immaginiamo un sistema che prova a recuperare una risorsa da più CDN:

async function fetchDaCdn() {
  const risposta = await Promise.any([
    fetch("https://cdn1.example.com/data"),
    fetch("https://cdn2.example.com/data"),
    fetch("https://cdn3.example.com/data")
  ]);
  return risposta.json();
}

Qui non importa quale CDN risponde. L'importante è ottenere il dato il prima possibile. Questo pattern è molto potente in sistemi distribuiti, dove la ridondanza è usata per aumentare affidabilità e prestazioni.

Quando .any fa davvero la differenza

Uno dei casi più interessanti riguarda la resilienza. Se un servizio è instabile, si possono lanciare più richieste in parallelo verso endpoint equivalenti. .any permette di ottenere rapidamente una risposta valida senza attendere tutte le altre. Questo migliora sia la velocità percepita sia la robustezza del sistema.

Va però ricordato che, se tutte le Promise falliscono, .any restituisce un errore speciale chiamato AggregateError che contiene tutte le ragioni di fallimento. Questo dettaglio è importante per il debugging.

.race: quando conta il primo risultato, qualunque sia

Promise.race è il combinatore più "estremo". Restituisce il risultato della prima Promise che si completa, sia essa risolta o rifiutata. Questo lo rende molto diverso da .any, che ignora i fallimenti finché non sono tutti tali.

Un caso d'uso classico è la gestione dei timeout:

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("Timeout")), ms);
  });
}
async function fetchConTimeout(url) {
  return Promise.race([
    fetch(url),
    timeout(3000)
  ]);
}

Qui la prima Promise che si completa determina l'esito. Se la fetch è lenta, il timeout interviene e l'operazione viene interrotta. Questo pattern è estremamente utile per evitare che operazioni lente blocchino il sistema.

.race in scenari più complessi

Oltre ai timeout, .race può essere usato per gestire competizioni tra task asincroni. Per esempio, si può avviare una richiesta di rete e contemporaneamente un recupero da cache, prendendo il risultato più veloce.

Questo tipo di approccio permette di ottimizzare la reattività delle applicazioni, specialmente in contesti dove la latenza è variabile.

Come scegliere il combinatore giusto

La scelta tra questi combinatori non è mai casuale. Dipende dal significato logico delle operazioni che si stanno coordinando.

Quando tutte le operazioni sono necessarie .all è la scelta naturale. Quando ogni risultato ha valore indipendente .allSettled offre maggiore flessibilità. Quando esistono alternative equivalenti .any permette di ottenere rapidamente un risultato valido. Quando invece si vuole reagire al primo evento disponibile, indipendentemente dal suo esito, .race diventa lo strumento più adatto.

Capire queste differenze significa scrivere codice più espressivo che racconta chiaramente l'intenzione del programma.

Un esempio combinato: orchestrare più strategie

Per chiudere, vale la pena osservare uno scenario più realistico in cui più combinatori convivono. Immaginiamo un sistema che prova a caricare dati da più fonti, ma con fallback e tolleranza agli errori.

async function caricaDati() {
  try {
    const risposta = await Promise.any([
      fetch("https://primary.api/data"),
      fetch("https://backup.api/data")
    ]);

    const dati = await risposta.json();
    const extra = await Promise.allSettled([
      fetch("/api/suggestions").then(r => r.json()),
      fetch("/api/ads").then(r => r.json())
    ]);

    return {
      dati,
      extra
    };
  } catch (e) {
    throw new Error("Impossibile recuperare i dati principali");
  }
}

In questo esempio .any .allSettled

Questo tipo di composizione è molto comune nelle applicazioni moderne.

Perché i combinatori cambiano il modo di scrivere asincrono

I combinatori di Promise permettono di modellare il comportamento asincrono in modo dichiarativo, evitando codice complesso fatto di callback, flag e stati intermedi. Quando vengono usati bene, rendono il codice più leggibile e più vicino al modo in cui si pensa il problema.

Nel tempo, imparare a scegliere il combinatore giusto diventa una competenza fondamentale. Non solo per scrivere codice corretto, ma per costruire applicazioni più robuste, reattive e resilienti.

Ti consigliamo anche