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.
