Exceptions / Undantag

Exceptions, eller undantag som det heter på svenska, skapar man då man stöter på något som är så fel eller ovanligt att man inte kan fortsätta med det man skulle göra. I språk som Java säger man att man kastar undantag (throw), i andra språk kan det heta raise. Man höjer alltså en flagga för att signalera att metoden inte avslutades så som den normalt brukar avslutas.

Om man till exempel ska skriva något i en fil men upptäcker att filen inte finns eller att disken är full så är det inte säkert att metoden som ska skriva till filen vet hur den ska hantera denna märkliga situation. Den kastar då ett undantag till den som anropade metoden och låter anroparen besluta vad som ska göras.

Undantag skickas inte via de vanliga returvärdena från en metod, utan normalt finns en separat kanal för att hantera dessa. När ett undantag kastas kommer koden därefter att hoppas över. Man hoppar frammåt i programmet tills dess att man hittar en speciell kodbit som hanterar felet som uppstått. Det är vanligt att programmeringsspråk har ett speciellt sätt att kapsla in kod som kan generera undantag så att det vet direkt var någonstans koden som hanterar det finns. Om koden för att hantera undantaget finns i samma metod som det genereras så hanteras det lokalt, annars kommer metoden att avslutas och undantaget skickas vidare till den anropande koden.

Exempel i Java

    try {
       FileOutputStream foStr = new FileOutputStream("foo");
    }
    catch (FileNotFoundException e) {
       System.err.println("File not found!");
    }
  

Hur och var undantag bör hanteras

Mitt generella tips när det gäller var man ska fånga undantag är: Fånga och hantera undantag så tidigt som möjligt. Om du kan hantera undantaget i samma metod som orsakar det så ska du fånga det där. Skicka inte vidare undantag som kod utanför metoden inte behöver känna till. Att läcka undantag är att avslöja implementationsdetaljer, och det ska man aldrig göra.

Om du försöker öppna en fil och den till exempel inte finns kommer du få ett FileNotFoundException från Java. Om detta inträffar i en metod där du har möjlighet att åtgärda felet (till exempel genom att skapa filen eller öppna någon annan fil) så är detta rätt ställe att ta hand om det. Fånga det då enligt exemplet ovan och gör rätt sak. Det är i princip aldrig rätt sak att ignorera undantag genom att fånga dem och lämna hanteringskoden tom. Undantag genereras för att något har gått snett, att ignorera detta är nästan alltid en bugg.

Om det inte går att hantera undantaget på något vettigt sätt i den metod där det genereras så bör det skickas vidare till den anropande koden. I det fallet fångar man inte undantaget alls, utan deklarerar att metoden kan kasta ett undantag med throws.

    public void readFile() throws FileNotFoundException
    {
       FileOutputStream foStr = new FileOutputStream("foo");
       ...
    }
  

Om man ska sätta en hel metod inom try{} eller bara det specifika anrop som genererar undantaget beror helt på vad metoden gör i övrigt och hur den ser ut rent estetiskt. Om man har flera anrop som kan generera undantag och alla dessa ska hanteras på samma sätt så är det ju vettigt att sätta alla dessa anrop inom samma try{} så att man bara behöver skriva hanteringskoden på ett ställe. Om det är flera anrop som genererar olika undantag som ska hanteras på olika sätt så tycker inte jag att de ska sitta i samma try{} även om det är möjligt i de flesta fall. Egentligen handlar det nog mest om en koddesign-fråga om man vill sätta hanteringskoden nära stället där undantaget genereras eller om man vill sätta all hanteringskod för en hel metod i slutet av metoden. Det viktigaste är ju att koden blir lättläst, så om det stökar till i koden att sätta en massa hanteringskod överallt så är det kanske bättre att sätta allt sist genom att sätta hela metoden i samma try{}.

När ska man använda undantag?

Att kasta undantag är som namnet antyder något man endast gör i undantagsfall. Fel som kan hanteras lokalt bör också hanteras lokalt, man ska inte lasta över ansvaret på någon annan metod om det inte är nödvändigt. Detta betyder även att man inte ska hantera andras fel. Får man till exempel felaktiga indata så är det högst motiverat att tala om att detta skedde, även om man kan hantera felet själv. Felaktiga indata är oftast något som den anropande metoden vill veta om eftersom det antyder att något är fel på riktigt. Att kasta ett undantag är ett utmärkt sätt att hantera felaktiga indata.

Det finns två regler man aldrig bör bryta emot:

    1. Fånga aldrig undantag om du inte vet varför de uppstår.
  

Om man inte vet varför ett undantag uppstår är det ganska troligt att man inte kan hantera det på ett vettigt sätt. Om man får ett undantag tillbaka från ett metodanrop så betyder det att något gått fel där. En felaktig hantering av detta undantag (till exempel att ignorera det) är det samma som att gömma fel i programmet. Kan man inte hantera felet är det bättre att skicka det vidare.

Som utvecklare är det ganska vanligt att man sitter längst ut i anropskedjan. Det vill säga man använder biblioteksfunktioner som kan generera undantag, men det finns ingen anropande kod utanför den egna som kan hantera felen. Regeln gäller naturligtvis även då - fånga inte undantaget om du inte vet varför det uppstår. Men då det inte finns någon att skicka vidare felet till blir du så illa tvungen att själv ta reda på vad som faktiskt händer och hantera undantaget på riktigt. Den som ser detta som en dålig sak kan sluta läsa nu och bör överväga att låta bli att programmera.

    2. Fånga rätt typ av undantag.
  

Den andra regeln är också till för att förhindra att man gömmer fel i sitt program. Säg till exempel att man (i Java) vill ta hand om FileNotFoundException. Då är det just detta man ska fånga, inte det mer generella IOException vilket kanske kan kännas lockande ibland. Att fånga IOException och bara hantera felet att filen inte hittas gör att saker kommer att gå sönder om andra I/O-fel inträffar. Av samma skäl fångar man därför aldrig RuntimeException eller Exception.

Regel två vill även förhindra att man fångar undantag av typen NullPointerException och ArithmeticException. Dessa indikerar att man har ett fel i sitt program och man ska inte fånga dem, man ska se till att de aldrig uppstår! NullPointerException får man om man försöker följa en referens som är NULL och ArithmeticException får man till exempel vid division med noll.

Man ska alltid se till att hantera de fel som man vet kan uppstå. Innan man utför en division ska man alltid försäkra sig om att man inte har noll i nämnaren. Om man upptäcker att det är en nolla så kan det mycket väl vara rätt sak att hantera detta genom att generera ett undantag. Men det ska då vara ett som talar om att den som skickade in nollan gjorde fel, inte ArithmeticException som säger att du gjorde fel som försökte använda nollan i en division.

Ibland ser man kod där undantag används som ett sätt att styra kontrollflödet i programmet. Man kastar undantag i vanligt förekommande situationer enbart därför att man inte orkar tänka ut en riktig lösning på ett problem. Detta är (enligt mig) fel och är ett exempel på dålig programmdesign.