Als software-ontwikkelaar heb je constant te maken met exceptions: gooien, afvangen, herstellen, meldingen tonen aan de eindgebruiker enzovoorts. Omdat je er zo veel mee te maken hebt, is het erg fijn als je er op een prettige manier mee kan werken.
Maar helaas zie ik het regelmatig ‘mis’ gaan, waardoor je op een erg moeilijke manier -of helemaal niet- van een bepaalde exception kan herstellen. Dat is niet fijn voor jouw ervaring als ontwikkelaar. Maar ook niet voor je eindgebruiker, want die krijgt de fout ‘Er is iets mis gegaan’ waar hij of zij niks mee kan. Ik ga een aantal fouten toelichten en uitleggen hoe ik die oplos. Daarmee zorg ik voor zowel een prettige ontwikkelaarservaring (DX) als een aangename eindgebruikerservaring (UX).
Misbruik van de \Exception-class
De meest gemaakte fout die ik zie, is dat er overal de \Exception-class wordt gebruikt (of één van de andere base-classes zoals bijvoorbeeld \RuntimeException). Het probleem daarmee is dat je geen idee hebt wat er mis is gegaan: praktisch iedere exception is een instantie van deze class. Daardoor kun je dus niet bepalen of je van deze exception kan herstellen, of dat de gebruiker een foutmelding te zien moet krijgen.
Neem het volgende voorbeeld uit de badcow/dns-library:
/**
* @throws \Exception
*/
public function fromText(string $text): void
{
$iterator = new \ArrayIterator(explode(' ', $text));
while ($iterator->valid()) {
$matches = [];
if (1 !== preg_match('/^(?!)?[1-2]:(?.+)$/i', $iterator->current(), $matches)) {
throw new \Exception(sprintf('"%s" is not a valid IP range.', $iterator->current()));
}
$ipBlock = IPBlock::create($matches['block']);
$this->addAddressRange($ipBlock, '!' !== $matches['negate']);
$iterator->next();
}
}
Als ik een formulier heb waar de gebruiker een IP-range invoert die niet geldig is, hoe kan ik deze exception dan afvangen en daar een nette melding van maken?
Dat kan alleen door alle exceptions af te vangen, te controleren of het bericht ‘is not a valid IP range’ bevat en zo niet de exception opnieuw gooien. Dat ziet er dan zo uit:
try {
$apl->fromText(...);
} catch (\Exception $e) {
if (str_contains($e->getMessage(), 'is not a valid IP range')) {
// Error tonen aan gebruiker ...
} else {
throw $e;
}
}
Terwijl het ook zo had gekund:
try {
$apl->fromText(...);
} catch (\InvalidIpRange $e) {
// Error tonen aan gebruiker ...
}
Het tweede voorbeeld leest een stuk prettiger, heeft minder code nodig en is een stuk robuuster. Stel de library past de exceptionmessage aan, dan werkt het eerste voorbeeld opeens niet meer. En daar zal je niet snel achterkomen, omdat het statisch niet te analyseren valt. Als de library de Exception-class aanpast, verwijdert of verplaatst dan weet je dat vrijwel direct: door statische analysators of door een ‘class not found’-foutmelding. Een Exception-class is een contract tussen jou en de library – een string niet.