Gestione Promesse

Edoardo Midali
Edoardo Midali

Le Promise in TypeScript rappresentano il meccanismo standard per gestire operazioni asincrone, fornendo un oggetto che rappresenta il completamento futuro di un’operazione. Una Promise può trovarsi in tre stati: pending (in attesa), fulfilled (risolta con successo), o rejected (rifiutata con errore), permettendo gestione elegante di codice asincrono attraverso chaining, composizione, e integrazione con async/await.

Creazione di Promise

Le Promise si creano con il costruttore Promise che riceve una funzione executor con callback resolve e reject.

// Promise base
const promise = new Promise<string>((resolve, reject) => {
  setTimeout(() => {
    resolve("Completato");
  }, 1000);
});

// Promise con tipo esplicito
function fetchData(): Promise<User> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id: 1, nome: "Mario" });
    }, 1000);
  });
}

// Promise rejected
function failingOperation(): Promise<never> {
  return new Promise((_, reject) => {
    reject(new Error("Operazione fallita"));
  });
}

// Promise risolta immediatamente
const resolved = Promise.resolve(42);
const rejected = Promise.reject(new Error("Errore"));

Then e Catch

I metodi then e catch gestiscono risoluzione e rigetto delle Promise.

// Then per successo
fetchData().then((user) => {
  console.log(user.nome);
});

// Catch per errori
fetchData()
  .then((user) => console.log(user))
  .catch((error) => console.error(error));

// Then con due callback
promise.then(
  (result) => console.log("Success:", result),
  (error) => console.error("Error:", error)
);

// Chaining
fetchUser(1)
  .then((user) => fetchPosts(user.id))
  .then((posts) => posts.filter((p) => p.published))
  .then((published) => console.log(published))
  .catch((error) => console.error(error));

// Trasformazione dati
fetchData()
  .then((user) => user.nome)
  .then((nome) => nome.toUpperCase())
  .then((upperName) => console.log(upperName));

Finally

Il metodo finally esegue sempre, indipendentemente da risoluzione o rigetto.

// Finally per cleanup
fetchData()
  .then((data) => processData(data))
  .catch((error) => console.error(error))
  .finally(() => {
    console.log("Operazione completata");
    hideLoader();
  });

// Finally non modifica valore
promise
  .then((value) => value * 2)
  .finally(() => console.log("Cleanup"))
  .then((value) => console.log(value)); // Valore da then, non finally

Promise.all

Esegue multiple Promise in parallelo, restituendo array risultati quando tutte completano.

// Promise.all base
const promise1 = fetchUser(1);
const promise2 = fetchUser(2);
const promise3 = fetchUser(3);

Promise.all([promise1, promise2, promise3])
  .then((users) => {
    console.log("Tutti gli utenti:", users);
  })
  .catch((error) => {
    console.error("Almeno una fallita:", error);
  });

// Con tipizzazione
async function loadMultiple(): Promise<[User, Post[], Comment[]]> {
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),
    fetchPosts(),
    fetchComments(),
  ]);

  return [user, posts, comments];
}

// Genera dinamicamente
const ids = [1, 2, 3, 4, 5];
const promises = ids.map((id) => fetchUser(id));

Promise.all(promises).then((users) => console.log(users));

Promise.allSettled

Attende completamento di tutte le Promise, restituendo risultati sia per successi che fallimenti.

// AllSettled
const promises = [
  Promise.resolve(1),
  Promise.reject("Errore"),
  Promise.resolve(3),
];

Promise.allSettled(promises).then((results) => {
  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      console.log(`${index}: Success -`, result.value);
    } else {
      console.log(`${index}: Failed -`, result.reason);
    }
  });
});

// Filtrare solo successi
Promise.allSettled(promises).then((results) => {
  const successi = results
    .filter(
      (r): r is PromiseFulfilledResult<number> => r.status === "fulfilled"
    )
    .map((r) => r.value);

  console.log(successi);
});

Promise.race

Restituisce la prima Promise che completa (risolta o rifiutata).

// Race base
const fast = new Promise((resolve) => setTimeout(() => resolve("Fast"), 100));
const slow = new Promise((resolve) => setTimeout(() => resolve("Slow"), 1000));

Promise.race([fast, slow]).then((result) => console.log(result)); // "Fast"

// Timeout implementation
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), timeoutMs)
  );

  return Promise.race([promise, timeout]);
}

// Uso
withTimeout(fetchData(), 5000)
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

Promise.any

Restituisce la prima Promise risolta, ignorando quelle rifiutate.

// Any - prima risoluzione
const promises = [
  Promise.reject("Errore 1"),
  Promise.resolve("Success"),
  Promise.reject("Errore 2"),
];

Promise.any(promises)
  .then((result) => console.log(result)) // "Success"
  .catch((error) => console.error(error));

// Fallback tra servizi
Promise.any([
  fetch("https://api1.example.com/data"),
  fetch("https://api2.example.com/data"),
  fetch("https://api3.example.com/data"),
])
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch(() => console.error("Tutti i servizi falliti"));

Async/Await con Promise

Async/await fornisce sintassi sincrona per codice asincrono basato su Promise.

// Async function
async function loadUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

// Gestione errori
async function loadUserSafe(id: number): Promise<User | null> {
  try {
    const user = await loadUser(id);
    return user;
  } catch (error) {
    console.error("Load failed:", error);
    return null;
  }
}

// Sequential vs parallel
async function sequential() {
  const user = await fetchUser(1); // Attende
  const posts = await fetchPosts(); // Attende user
  return { user, posts };
}

async function parallel() {
  const [user, posts] = await Promise.all([fetchUser(1), fetchPosts()]);
  return { user, posts };
}

Chaining Complesso

Combinare multiple operazioni asincrone in catene complesse.

// Pipeline asincrona
async function processUserData(userId: number) {
  return fetchUser(userId)
    .then((user) => {
      return Promise.all([
        Promise.resolve(user),
        fetchPosts(user.id),
        fetchSettings(user.id),
      ]);
    })
    .then(([user, posts, settings]) => {
      return {
        user,
        posts: posts.filter((p) => p.published),
        settings,
      };
    })
    .catch((error) => {
      console.error("Pipeline failed:", error);
      throw error;
    });
}

// Conditional chaining
function conditionalLoad(condition: boolean): Promise<string> {
  let promise = Promise.resolve("Start");

  if (condition) {
    promise = promise.then(() => fetchAdditionalData());
  }

  return promise.then((data) => processData(data));
}

Retry Pattern

Implementare retry automatici per operazioni che possono fallire temporaneamente.

// Retry con delay
async function retry<T>(
  fn: () => Promise<T>,
  maxAttempts: number = 3,
  delayMs: number = 1000
): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts) {
        throw error;
      }

      console.log(`Attempt ${attempt} failed, retrying...`);
      await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
    }
  }

  throw new Error("Max attempts reached");
}

// Uso
retry(() => fetch("/api/data").then((r) => r.json()), 3, 500)
  .then((data) => console.log(data))
  .catch((error) => console.error("All retries failed:", error));

// Retry con backoff esponenziale
async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxAttempts: number = 5
): Promise<T> {
  let lastError: Error;

  for (let i = 0; i < maxAttempts; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error instanceof Error ? error : new Error("Unknown");
      const delay = Math.min(1000 * Math.pow(2, i), 10000);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }

  throw lastError!;
}

Promise Queue

Gestire esecuzione sequenziale di Promise per limitare concorrenza.

// Queue sequenziale
class PromiseQueue {
  private queue: Array<() => Promise<any>> = [];
  private running = false;

  add<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          const result = await fn();
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });

      this.run();
    });
  }

  private async run() {
    if (this.running || this.queue.length === 0) return;

    this.running = true;

    while (this.queue.length > 0) {
      const fn = this.queue.shift()!;
      await fn();
    }

    this.running = false;
  }
}

// Uso
const queue = new PromiseQueue();

queue.add(() => fetchUser(1));
queue.add(() => fetchUser(2));
queue.add(() => fetchUser(3));

Cancellazione

Implementare cancellazione di operazioni asincrone con AbortController.

// Cancellazione con AbortController
async function fetchWithCancel(url: string, signal: AbortSignal): Promise<any> {
  const response = await fetch(url, { signal });
  return await response.json();
}

const controller = new AbortController();

fetchWithCancel("/api/data", controller.signal)
  .then((data) => console.log(data))
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("Fetch cancellato");
    }
  });

// Cancella dopo 5 secondi
setTimeout(() => controller.abort(), 5000);

// Promise wrapper cancellabile
class CancellablePromise<T> {
  private controller = new AbortController();

  constructor(private executor: (signal: AbortSignal) => Promise<T>) {}

  execute(): Promise<T> {
    return this.executor(this.controller.signal);
  }

  cancel(): void {
    this.controller.abort();
  }
}

Conclusioni

Le Promise in TypeScript forniscono astrazione elegante per operazioni asincrone, permettendo gestione chiara di successo e fallimento attraverso then, catch, e finally. Metodi come Promise.all, allSettled, race, e any abilitano composizione sofisticata di operazioni parallele, mentre integrazione con async/await offre sintassi sincrona per codice asincrono. Pattern come retry, queue, e cancellazione estendono funzionalità base per scenari complessi, risultando essenziale per applicazioni moderne che gestiscono I/O, network, e altre operazioni non-bloccanti con robustezza e leggibilità.