Strategier för felhantering
Detta inlägg argumenterar för att mönstret
try {
...
} catch(SomeCheckedException e) {
e.printStackTrace();
}
är ett anti-pattern och definitivt ej bör rekommenderas som ett generellt felhanteringsmönster.
Vad är det då för fel på denna konstruktion? Jo, problemet är att den bryter mot en bra grundprincip inom felhantering, ”fail fast”.
Principen ”fail fast” bygger på att man avbryter programmet så fort ett fel uppstår. Huvudorsaken till att avbryta tidigt är att undvika följdfel. Samtidigt så undviks maskeringar av fel, vilket gör det lättare att diagnostisera och korrigera brister. Följs denna teknik och kombineras med vältäckande tester kan man eliminera många buggar och uppnå en hög genomgående kvalité i sin produkt.
När denna princip är på plats kan man sedan välja göra programmet mer flexibelt genom att selektivt lägga till felhanteringsstrategier.
Detta görs ofta genom att lägga till alternativa strategier för specifika felfall. Detta benämns ofta som ”graceful degradation”. Som ett exempel kan man tänka sig en e-handel som köar upp ordrar då kontakten med banken faller bort. Har man en sådan ”plan b” så har man täckt upp för det specifika felfallet att kontakten med banken faller bort. Ett annat exempel är att falla tillbaka till en enklare webbsida då en plug-in saknas eller en äldre browser används.
För oplanerade fel, sådana där man ofta inte har någon uttänkt strategi, är det bästa man kan göra att återställa systemet till ett känt säkert läge. Ett allmängiltigt sätt att skicka ett program till ett känt läge är att avbryta det. Är ditt system inte livsuppehållande eller på annat sätt kritiskt är detta läge även säkert. Så en bra grundstrategi är att avbryta programmet när ett okänt fel uppstår.
Tillbaka då till strategin att anropa printStackTrace(). Denna felhanterare gör två saker:
- Skriver ut en felrapport
- Fortsätter exekveringen av programmet
Det är i huvudsak punkt två som bryter mot ”fail fast”. Om man har en ”graceful degradation” eller ”fail safe”-strategi på plats så bör man när man fångat felet anropa ”plan b”, inte bara fortsätta. Är felet istället oplanerat så bör man så snart som möjligt försätta programmet i ett säkert läge och skriva ut en felrapport.
I printStackTrace
-fallet försätts inte programmet i ett säkert läge. Programmet tillåts fortsätta att exekvera och kommer förmodligen trigga följdfel. I avseendet följdfel är därmed denna form av felhantering inte bättre än det välkända anti-patternet att svälja felet helt:
try {
...
} catch(SomeCheckedException e) {
//dålig taktik
//no-op
}
Inte heller taktiken att skriva ut felrapporten på en logg undslipper detta argument:
try {
...
} catch(SomeCheckedException e) {
//dålig taktik
logger.error(e);
}
En tredje teknik som förekommer är log and throw:
try {
...
} catch(SomeCheckedException e) {
//dålig taktik
logger.error(e);
throw new RuntimeException(e);
}
Att kasta vidare räcker gott och väl, annars riskerar man flera utskrifter för samma fel.
Man vill alltså försätta programmet i ett säkert läge. Dessutom vill man ha en felutskrift utan duplikat. Vän av ordning frågar sig såklart hur man åstadkommer detta på ett enkelt sätt. Svaret är enkelt: kasta exceptionet vidare:
try {
...
} catch(SomeCheckedException e) {
//bra taktik
throw new RuntimeException(e);
}
Vad åstadkommer en sådan konstruktion? Jo, felet kommer att propageras uppåt i stacken tills det når trådens topp, där ett stacktrace skrivs ut innan tråden avslutas. Detta beteende är inbyggt i Java. Har du en enkel applikation med endast en non-deamon-tråd så kommer programmet även att avslutas i samband med att ditt fel skrivs ut. Fallet multitrådade program är mer invecklat och tas inte upp i detta inlägg.
För att på ett enkelt sätt kunna följa dessa principer rekommenderar jag att du ändrar mallen för ”surround with try-catch” i ditt IDE så att den kastar checked exceptions vidare, istället för att anropa printStackTrace()
.
I Eclipse kan du göra detta i inställningen Java > Code Style > Code Templates
och ändra från default
// ${todo} Auto-generated catch block
${exception_var}.printStackTrace();
till
throw new RuntimeException(${exception_var});
På detta sätt får du den bra taktiken per default när du gör ”quick fix”!