cat swift/error-modeling-throws-result.md
Error modeling con throws, Result ed errori custom
Gli errori non sono solo eccezioni tecniche: nel dominio Swift diventano stati falliti espliciti, nominati e testabili.
Nel modulo precedente abbiamo usato optionals per rappresentare assenza o creazione fallita. Ma non tutti i fallimenti sono uguali.
Se un coupon non può essere applicato, vogliamo sapere perché:
- codice scaduto;
- carrello sotto la soglia minima;
- categoria non presente;
- utente non autorizzato;
- coupon già usato.
Restituire nil perderebbe informazione.
Optional: troppo povero per molti errori
func applica(coupon: Coupon, a carrello: Carrello) -> Carrello? {
...
}
Se ritorna nil, cosa è successo? Non lo sappiamo.
Meglio modellare l’errore:
enum ErroreCoupon: Error {
case carrelloSottoMinimo(richiesto: Int, attuale: Int)
case categoriaMancante(CategoriaProdotto)
case couponScaduto
case utenteNonAutorizzato
}
Ora il fallimento ha un linguaggio.
throws
throws è adatto quando vuoi scrivere codice lineare e interrompere il flusso al primo errore.
func valida(_ coupon: Coupon, nel contesto: ContestoCoupon) throws {
for regola in coupon.regole {
try valida(regola, nel: contesto)
}
}
La funzione può completare normalmente oppure lanciare un errore.
Uso:
do {
try valida(coupon, nel: contesto)
print("Coupon valido")
} catch let errore as ErroreCoupon {
print("Coupon non valido: \(errore)")
} catch {
print("Errore inatteso")
}
Errori custom
Gli errori dovrebbero parlare la lingua del dominio.
enum ErroreRegolaCoupon: Error, Equatable {
case minimoNonRaggiunto(richiesto: Int, attuale: Int)
case categoriaRichiestaAssente(CategoriaProdotto)
case nonPrimoOrdine
}
Questo è molto più utile di:
throw NSError(domain: "Coupon", code: 1)
Nel codice Swift moderno, preferisci errori tipizzati è locali al dominio.
Validare una regola
func valida(_ regola: RegolaCoupon, nel contesto: ContestoCoupon) throws {
switch regola {
case .minimoCarrello(let cent):
guard contesto.totaleCarrelloInCent >= cent else {
throw ErroreRegolaCoupon.minimoNonRaggiunto(
richiesto: cent,
attuale: contesto.totaleCarrelloInCent
)
}
case .categoriaRichiesta(let categoria):
guard contesto.categorieNelCarrello.contains(categoria) else {
throw ErroreRegolaCoupon.categoriaRichiestaAssente(categoria)
}
case .primoOrdineSolo:
guard contesto.utenteHaGiaOrdinato == false else {
throw ErroreRegolaCoupon.nonPrimoOrdine
}
}
}
La funzione non restituisce Bool. Se fallisce, spiega perché.
Result
Result<Success, Failure> è utile quando vuoi trattare successo e fallimento come valore.
func risultatoValidazione(
_ coupon: Coupon,
nel contesto: ContestoCoupon
) -> Result<Coupon, ErroreRegolaCoupon> {
do {
try valida(coupon, nel: contesto)
return .success(coupon)
} catch let errore as ErroreRegolaCoupon {
return .failure(errore)
} catch {
return .failure(.nonPrimoOrdine)
}
}
L’ultimo catch qui è solo un esempio povero: in codice reale non dovresti trasformare errori inattesi in un caso falso. Una soluzione migliore e progettare funzioni che lanciano solo errori attesi o usare un errore più ampio.
throws o Result?
Usa throws quando:
- il flusso e lineare;
- vuoi propagare errori con
try; - l’errore interrompe l’operazione.
Usa Result quando:
- vuoi salvare il risultato per dopo;
- vuoi passarlo a un’altra funzione;
- vuoi modellare successo/fallimento dentro una collection;
- vuoi rendere il fallimento parte di un valore.
Esempio:
let risultati: [Result<Coupon, ErroreRegolaCoupon>] = couponDisponibili.map { coupon in
risultatoValidazione(coupon, nel: contesto)
}
Errori multipli
A volte vuoi raccogliere tutti gli errori, non fermarti al primo.
struct EsitoValidazione<Valore, Errore: Error> {
let valore: Valore
let errori: [Errore]
var valido: Bool {
errori.isEmpty
}
}
Questo tipo è utile per form, regole multiple e diagnostica.
func validaTutte(
_ regole: [RegolaCoupon],
nel contesto: ContestoCoupon
) -> [ErroreRegolaCoupon] {
regole.compactMap { regola in
do {
try valida(regola, nel: contesto)
return nil
} catch let errore as ErroreRegolaCoupon {
return errore
} catch {
return nil
}
}
}
Checklist
Usa nil solo quando basta sapere che un valore manca.
Usa errori custom quando:
- il motivo del fallimento conta;
- l’utente deve vedere un messaggio specifico;
- il test deve verificare il caso esatto;
- il dominio ha regole esplicite.
Usa Result quando il fallimento deve viaggiare come valore.
Esercizio
Definisci:
ErroreRegolaCoupon;func valida(_ regola: RegolaCoupon, nel contesto: ContestoCoupon) throws;func valida(_ coupon: Coupon, nel contesto: ContestoCoupon) throws;func validaTutte(...) -> [ErroreRegolaCoupon].
Poi crea tre coupon non validi e verifica che ogni errore abbia un caso preciso.