JavaScript Async/Await: rendere il codice asincrono più chiaro con il pattern “semaforo”

by Anastasia P.
SHARE
JavaScript Async/Await: rendere il codice asincrono più chiaro con il pattern “semaforo”
© Guida-HTML5.it

Introduzione

Quando lavoriamo con Async/Await, il codice diventa molto più leggibile rispetto alle catene di Promise, ma non sempre il problema è la sintassi. In molte applicazioni reali, infatti, il vero rischio è avviare troppe operazioni asincrone insieme: doppio click su un pulsante, invii ripetuti di un form, richieste API duplicate, sincronizzazioni concorrenti sullo stesso dato.

In questi casi è utile un sotto-argomento molto pratico: il pattern semaforo (o lock semplice). L’idea è impedire che una funzione asincrona venga eseguita contemporaneamente più volte. In altre parole, se un’operazione è già in corso, le richieste successive vengono ignorate, accodate o gestite in modo controllato.

Questo pattern è perfetto per rendere il codice asincrono più leggibile e sicuro, soprattutto quando devi proteggere azioni come:

  • salvataggio di un documento;
  • invio di dati a un’API;
  • aggiornamento di stato lato client;
  • download o caricamento di risorse;
  • azioni UI che non devono partire due volte.

Codice completo

// Esempio: proteggere una funzione asincrona da esecuzioni concorrenti
// Scenario: un pulsante "Salva" che non deve inviare due richieste contemporanee

class SaveManager {
  constructor() {
    this.isSaving = false;
    this.lastSavedAt = null;
  }

  async saveDocument(documentData) {
    // Se c´è già un salvataggio in corso, blocchiamo l´azione
    if (this.isSaving) {
      console.log("Salvataggio già in corso. Ignoro la nuova richiesta.");
      return {
        ok: false,
        reason: "already_saving"
      };
    }

    this.isSaving = true;

    try {
      console.log("Avvio salvataggio...");

      // Simuliamo una chiamata API lenta
      const response = await this.fakeApiSave(documentData);

      this.lastSavedAt = new Date();

      console.log("Salvataggio completato:", response);

      return {
        ok: true,
        data: response,
        savedAt: this.lastSavedAt
      };
    } catch (error) {
      console.error("Errore durante il salvataggio:", error.message);

      return {
        ok: false,
        reason: "save_failed",
        error: error.message
      };
    } finally {
      // Il semaforo viene sempre rilasciato, anche in caso di errore
      this.isSaving = false;
      console.log("Semaforo rilasciato.");
    }
  }

  async fakeApiSave(documentData) {
    // Simula latenza di rete
    await new Promise(resolve => setTimeout(resolve, 1500));

    // Simula un errore casuale
    if (Math.random() < 0.2) {
      throw new Error("Server non disponibile");
    }

    return {
      id: "doc_123",
      title: documentData.title,
      updatedAt: new Date().toISOString()
    };
  }
}

// Uso reale
const manager = new SaveManager();

async function handleSaveClick() {
  const result = await manager.saveDocument({
    title: "Appunti Async/Await",
    content: "Contenuto del documento..."
  });

  if (result.ok) {
    console.log("UI: salvataggio riuscito");
  } else if (result.reason === "already_saving") {
    console.log("UI: bottone disabilitato o click ignorato");
  } else {
    console.log("UI: mostra messaggio di errore");
  }
}

// Simuliamo click ripetuti
handleSaveClick();
handleSaveClick();
handleSaveClick();

Spiegazione

Il codice sopra introduce una tecnica semplice ma molto efficace: una variabile booleana, isSaving, funziona da semaforo. Quando la funzione saveDocument parte, controlla se un’altra esecuzione è già attiva. Se sì, esce subito. Se no, imposta il flag a true e continua.

1. Il controllo di accesso

La prima parte del metodo è fondamentale:

if (this.isSaving) {
  return { ok: false, reason: "already_saving" };
}

Questo evita duplicazioni. È molto utile in interfacce utente, dove un utente può fare più click rapidi o generare eventi ripetuti. Senza questo controllo, potresti inviare due volte la stessa richiesta al server.

2. Il blocco try/catch/finally

Con Async/Await, try/catch/finally è il modo più pulito per gestire errori e cleanup:

  • try: contiene la logica asincrona principale;
  • catch: intercetta errori di rete, timeout applicativi o eccezioni lanciate manualmente;
  • finally: esegue sempre il rilascio del semaforo.

Il punto più importante è il finally: anche se la richiesta fallisce, isSaving torna a false. Senza questa parte, il sistema potrebbe restare bloccato per sempre.

3. Perché è più leggibile con Async/Await

Lo stesso comportamento, scritto con callback o catene di Promise, sarebbe più difficile da seguire. Con Async/Await invece il flusso appare quasi sincrono:

  • controllo iniziale;
  • imposto lo stato;
  • aspetto il risultato;
  • gestisco errore o successo;
  • rilascio il lock.

Questa sequenza lineare è molto più facile da mantenere, soprattutto quando il codice cresce e viene modificato da più persone.

4. Dove usare questo pattern

Il pattern semaforo è adatto quando vuoi garantire che una sola operazione alla volta possa accedere a una risorsa. Alcuni esempi concreti:

  • salvataggio di bozze in editor online;
  • sincronizzazione di configurazioni locali con un backend;
  • aggiornamento di profili utente;
  • scansione o importazione di file;
  • azioni critiche avviate da eventi UI.

Best practice

Il pattern è semplice, ma per usarlo bene conviene seguire alcune regole pratiche.

  • Usa sempre finally: è la protezione più importante per evitare lock permanenti.
  • Restituisci un risultato esplicito: invece di ignorare silenziosamente il secondo invio, torna un oggetto che spieghi il motivo del rifiuto.
  • Separa logica e interfaccia: il semaforo deve stare nella logica di business, mentre la UI può limitarsi a disabilitare il pulsante o mostrare un loader.
  • Non abusare del blocco globale: se hai più risorse indipendenti, usa un lock per risorsa, non uno solo per tutta l’applicazione.
  • Gestisci bene gli errori: il blocco non deve nascondere i problemi reali; deve solo impedire esecuzioni concorrenti.
  • Valuta la coda se serve: se non vuoi ignorare le richieste ripetute, puoi trasformare il semaforo in una coda semplice che esegue un’operazione dopo l’altra.

Un’altra buona pratica è aggiornare la UI in modo coerente. Per esempio, quando isSaving è true, puoi disabilitare il pulsante:

button.disabled = manager.isSaving;

In questo modo l’utente riceve un feedback visivo chiaro e non tenta di avviare più volte la stessa operazione.

Riepilogo

Async/Await non serve solo a rendere il codice più elegante: è anche uno strumento ottimo per costruire flussi asincroni più robusti. Il pattern semaforo è una soluzione semplice, leggibile e molto utile per evitare esecuzioni concorrenti indesiderate.

In sintesi:

  • controlli se un’operazione è già in corso;
  • blocchi le richieste successive;
  • esegui la logica asincrona con await;
  • rilasci sempre il lock con finally;
  • mantieni il codice facile da leggere e da mantenere.

È una tecnica piccola ma molto concreta, perfetta per migliorare la qualità del codice in progetti reali.

Approfondisci con risorse ufficiali

  • MDN Web Docs - async function: documentazione ufficiale sulle funzioni asincrone.
  • MDN Web Docs - Promise: per capire bene il modello su cui si basa Async/Await.
  • JavaScript.info - Async/await: spiegazioni chiare con esempi pratici.
  • Node.js Documentation: utile se lavori in ambiente server-side e vuoi gestire concorrenza e operazioni I/O.

SHARE