Programuj bez výjimek!

18. 07. 2020

Dlouho, dlouho mě trápí kód ovládaný výjimkami. Nikdy nevím jaká výjimka na mě vyskočí. A strach je podobný jako dopis od finančího úřadu. V PHP je pokus to řešit anotacemi:

/*
 * @return string
 *
 * @throws MyException
 */
public function getName()
{
    return "Baltazar"
}

Občas zapomeneš nějakou výjimku uvést, občas smazat a nikdo tě nenutí je tam psát.

Ale co je daleko horší, abys tohle správně zpracoval, musíš to trajkečnout, jen pro pořádek kolik je to psaní (raději v Typescriptu):

let name: string | undefined
try {
  name = getName()
} catch (err) {

}

Ano pro mě zásadní problém je let, které používat nechci - chci přiřadit jméno a dále ho neměnit, ovšem let mi to dovolí a chyba je na světě. Navíc celý konstrukt je dlouhý a zbytečně nečitelný.

Pak je tu dilema s tím, jaký kód do try catche vložit. Poučka zní, že by to měl být nezbytně nutný kód. A co když kečuji nad několika funkcemi, je to dobře? Nikdy si nejsem jistý.

Další důvod proč výjimky nepoužívat je jejich tendence probublávat vrstvami. Tohle probublání si může dovolit pouze LogicException. V praxi ale vidím, že probublá kde co, ale často i, že se ignoruje vůbec existence těchto dvou typů výjimek.

V ideálním kódu by měla každá vrtva zabalit chybu do nějaké abstrakce a předchozí chybu uložit někam k podrobnostem. Stejně tak jako každá vrstva vytváří abstrakci, musí stejnou abstrakci vytvořit i pro chyby.

interface Error {
  message: string
  previousError?: Error
}

Nechme tedy výjimky jejich původnímu smyslu a nechť shodí program. Nebude již více nutno rozlišovat RuntimeException a LogicException. Nyní všechny výjimky budou Logic. Jak elegantní.

Co dál? Typicky když po funkci chci odpověď na otázku zda dopadla dobře nebo špatně, vystačím si s návratovým typem boolean. Pokud mi nestačí při chybě vrátit false, ale musím rozlišovat mezi jednotlivými chybami, doteď jsem použil výjimku.

Nově však použiji návratovou hodnotu. Ovšem za cenu, že výsledek funkce nemohu vracet přímo ale v objektu.

const getNickByYourAge = (age: number): { result?: string, error?: string } => {
  if (age < 33) {
    return {
      error: 'Ask your mother for your nick.'
    }   
  }

  if (age > 33) {
    return {
      error: 'Ask your children for your nick.'
    } 
  }

  return {
    result: 'Son of god'
  }
}

Vyrazně jednodušeji pak mohu funkci ošetřit:

const nick = getNickByYourAge(30)
if (nick.error === 'Ask you mother for your nick.') {
  askMother()
}
console.log('Nick', nick.result)

Teď už to zbývá trochu doladit, např. abychom si byli jistí, že nevrátíme prázdný objekt {}.

export type FunctionResult<T> = { error: string; result?: undefined } | { error?: undefined; result: T }

A vesele tak můžeme použít u všech funkcí:

const getNickByYourAge = (age: number): FunctionResult<string> => {
  ...
}

Nehledě na programovací jazyk, výjimky nechávám na výjimečné situace. A ošetřování chyb je součástí zdrojového kódu a tak proč nepoužít obyčejné kostrukty.

Twitter, Facebook