cat swift/error-modeling-throws-result.md

Lezione 11 55 min

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.