1 maggio 2026

Far funzionare il “happy path” è solo l’inizio: progettare frontend che falliscono bene

L’ingegneria del software non è far andare il codice quando tutto va bene, ma gestire in modo affidabile ciò che inevitabilmente andrà storto.

Un frontend “finito” non è quello in cui il flusso ideale funziona, ma quello che regge gli errori: rete instabile, dati imprevisti, permessi mancanti, dipendenze giù. Vediamo come ragionare su rischi e probabilità, come progettare fallback e come testare davvero gli scenari critici senza trasformare il progetto in una collezione di patch.

Il lavoro non finisce quando “funziona”

Nel frontend è fin troppo facile considerare completata una feature quando:

  • il flusso principale (happy path) funziona;
  • la UI “sembra” corretta;
  • i dati arrivano e vengono renderizzati.

Ma questa è solo una piccola parte del lavoro. La differenza tra una demo e un prodotto sta in ciò che succede quando le cose vanno male: errori di rete, input inattesi, servizi esterni che degradano, utenti che fanno “cose strane”, concorrenza, timeout.

Un modo utile di pensarla è: l’ingegneria del software è scrivere codice che fallisce bene.

“Fallire bene” cosa significa, concretamente?

Un sistema che fallisce bene non evita tutti gli errori (impossibile), ma li gestisce in modo che:

  • l’utente capisca cosa sta succedendo;
  • l’app resti usabile o almeno non si rompa in modo incontrollato;
  • i dati non vengano corrotti;
  • l’impatto dell’errore sia contenuto;
  • sia possibile diagnosticare il problema (log/telemetria/monitoraggio).

In UI questo si traduce in dettagli spesso considerati “extra”, ma che sono parte del requisito di qualità: stati di errore curati, retry, fallback, messaggi utili, limiti e validazioni.

Priorità: probabilità vs gravità

Non tutti i problemi meritano lo stesso investimento. Una strategia pratica è ragionare su due assi:

  1. Cosa è probabile che vada storto?

    • rete lenta o intermittente;
    • API che rispondono con 500/503;
    • payload parziali o campi null;
    • utenti non autenticati o con token scaduto;
    • race condition tra più richieste (es. cambio rapido di filtri).
  2. Cosa sarebbe catastrofico se andasse storto?

    • azioni irreversibili senza conferma;
    • doppio invio di una transazione;
    • visualizzazione di dati di un altro utente;
    • perdita di modifiche non salvate;
    • stato locale incoerente che porta a bug a cascata.

Gli scenari “probabili” vanno coperti perché capitano spesso. Quelli “catastrofici” vanno coperti anche se rari, perché non puoi permetterteli.

Progettare per gli errori: pattern utili nel frontend

1) Stati espliciti: loading, empty, error, stale

Molti bug nascono dal trattare lo stato come binario: “ho dati / non ho dati”. In realtà serve modellare almeno:

  • loading (con skeleton o progress);
  • empty (nessun risultato, ma tutto ok);
  • error (con messaggio e azione: retry, contatta supporto, ecc.);
  • stale (mostro dati vecchi ma avviso che non sono aggiornati).

2) Retry e backoff (senza martellare)

Per errori transitori (rete, 503) ha senso offrire un retry. Può essere:

  • manuale (“Riprova”);
  • automatico con backoff e un limite (es. 3 tentativi).

La chiave è evitare loop infiniti e comunicare chiaramente cosa sta succedendo.

3) Fallback ragionevoli

Quando una dipendenza non è disponibile, chiediti: esiste una modalità degradata utile?

  • mostrare contenuti cache/precedenti;
  • disabilitare temporaneamente una sezione;
  • rendere read-only una schermata che normalmente è interattiva;
  • ridurre funzionalità non essenziali mantenendo il core.

4) Confini di contenimento (Error Boundary e simili)

Un errore runtime non dovrebbe buttare giù tutta l’app. Isola il rischio:

  • error boundary per porzioni di UI;
  • fallback per componenti critici;
  • routing che gestisce 404/500 con pagine dedicate.

5) Idempotenza e protezione da doppio invio

Molti “disastri” lato utente arrivano da:

  • click ripetuti;
  • refresh mentre una richiesta è in corso;
  • riconnessioni.

Mitigazioni tipiche:

  • disabilitare il bottone durante la submit;
  • usare request id / idempotency key quando possibile;
  • gestire correttamente lo stato “pending”.

Testare ciò che conta (non solo il percorso dorato)

Se l’obiettivo è fallire bene, i test devono coprire anche i fallimenti.

Test mirati sugli scenari probabili

  • timeout e lentezza (mock di network delay);
  • API che risponde con errori (401/403/500);
  • dati incompleti o null;
  • race condition (es. due richieste: vince l’ultima).

Test sugli scenari catastrofici

  • prevenzione di doppie operazioni;
  • autorizzazioni: non mostrare dati non permessi;
  • recupero da stato incoerente (es. reset controllato, refresh token, logout forzato).

Come capire se stai testando la cosa giusta

Un buon segnale è quando i test verificano non solo “non crasha”, ma:

  • che l’utente riceva un feedback;
  • che ci sia un percorso d’uscita (retry, annulla, contatta supporto);
  • che l’app rimanga navigabile.

“Prendersi cura degli utenti” è un requisito tecnico

Gestire gli errori non è solo UX: è affidabilità. Se qualcosa va storto, l’utente deve essere guidato e protetto.

Un’implementazione completa, in pratica, include:

  • messaggi utili (non “Errore generico” ovunque);
  • azioni disponibili (retry, torna indietro, salva bozza);
  • preservazione del lavoro (autosave, draft, warning su unsaved changes);
  • telemetria per capire cosa è successo davvero.

Conclusione

Far funzionare il happy path è necessario, ma non sufficiente. Un frontend “professionale” nasce quando identifichi ciò che può andare storto (probabile) e ciò che non puoi permetterti che vada storto (catastrofico), e costruisci attorno a questo:

  • stati chiari;
  • fallback e retry;
  • confini di contenimento;
  • test che includono il fallimento.

Il risultato non è solo un’app che funziona: è un’app che resta affidabile quando il mondo reale fa il mondo reale.