Pochopit výjimky (exceptions) není nic složitého; problém ale většinou je, že ten kdo je vysvětluje (zpravidla autor knihy či učitel) je sám moc dobře nechápe a tak na vás vychrlí hromadu teorie a doufá, že je (ne)pochopíte stejně, jako on.
Poznámka: tenhle článek pojmu trochu obecněji, takže zde najdete i ukázky z PHP či Javy, které výjimky používají častěji než JavaScript. V JavaScriptu se místo třídy Exception používá prototyp Error!
K čemu je vlastně výjimka?
Tohle je ta teorie, kterou můžete klidně přeskočit :).
Výjimky pocházejí z typových jazyků, kde musí každá funkce vracet konkrétní datový typ – a v případě chyby ho musí nějak vecpat do daného typu.
string getUserName() {
user = db->getUser();
return (user ? user->name : 'No user');
}
int getUserId(user) {
return (user ? user->id : -1);
}
bool isUserBanned(user) {
return (user ? user->hasBan : false)
}
Výše uvedené funkce (napsané ve smyšleném jazyce) řeší chyby tím, že použijí nějakou zvláštní hodnotu daného typu, který vrací:
- Místo jména uživatele vrátí „No user“ (a vědomě ignoruje fakt, že žádný uživatel se nesmí jmenovat „No user“).
- Místo čísla uživatele vrátí -1 (a předpokládá, že číslo uživatele je vždy kladné).
- Místo „vypočtené“ logické hodnoty vrátí prostě False (nebo True) podle toho, jaký je význam funkce (zde pokud uživatel neexistuje, nemůže mít ban).
Tohle je sice pěkné, a každý (i sebezkušenější) programátor to použije pro jednoduché ošetření chyby, ale co dělat v případě, že nám daný datový typ nestačí nebo ho použít nemůžeme:
int average(a,b,c) {
if (a && b && c) {
return (a+b+c)/3;
} else return -1;
}
Tahle funkce řeší problém stejně – pokud nemůže použít výpočet, vrátí hodnotu v rámci daného typu, tedy -1. Jak ale teď poznat, jestli se -1 vrátilo, protože vstupní data byla [1,2,] (třetí hodnota chybí a tudíž nelze průměr vypočítat) nebo [0,-1,-2] (a jde tedy skutečně o průměr hodnot).
V jazycích jako JavaScript nebo PHP se tohle snadno vyřeší tím, že funkce (nečekaně) vrátí jiný typ:
function average(a,b,c) {
if (a && b && c) {
return (a+b+c)/3;
} else return false;
}
To ale není příliš šikovné řešení, protože programátor musí pamatovat, že funkce nemusí vrátit to, co čeká; a v typových jazycích není vůbec možné (validátor nebo kompilátor prostě vyhodí „Type mismatch“, „Invalid return type“ apod.).
A od toho tu jsou právě výjimky.
V jednoduchosti je síla
Teď si ukážeme, jak snadno předchozí funkci přepsat s použitím výjimky.
int average(a,b,c) {
if (a && b && c) {
return (a+b+c)/3;
} else throw new Exception('Missing parameters - cannot count their average.');
}
Funkce v případě chyby použije příkaz throw (který podporují všechny jazyky obsahující výjimky) a za ním uvede, co chce vyhodit. Zde vytvoří nový objekt třídy Exception (která je také ve většině jazyků) a předá mu jako vstupní parametr požadovaný text chyby. Pokud pak dojde k chybě, (v závislosti na jazyce) se tento text zobrazí uživateli nebo vývojáři.
V JavaScriptu se místo Exception používá objekt Error, ale lze použít i zkrácený zápis:
throw new Error('Missing parameters');
throw 'Missing parameters';
Vyhození výjimky zpravidla znamená, že daný program „spadne“ a skončí nebo se nebude chovat správně. A z toho důvodu je zde i možnost výjimku zachytit, ošetřit a chybě se vyhnout.
try {
avg = average(x,y,z);
} catch (Exception e) {
avg = 0;
}
Funkci, která může výjimku vyhodit, zavřeme do bloku TRY, čímž řekneme, že se chceme postarat o chyby, ke kterým dojde. Následně v bloku CATCH zapíšeme kód, který případnou chybu opravuje. A to je vše.
Proč používat výjimky?
Někdo možná řekne „Proč se zdržovat vyhazováním a ošetřováním výjimek, když to jde udělat i jednodušeji?“ a často si ani neuvědomí, že v případě výjimek toho o nic víc nenapíše a přitom získá mnohem lepší nástroj.
Zkuste porovnat tyhle dva kódy:
//Simple error
function add(a,b) {
if (-1 < a && -1 < b) {
return a + b;
}
else return -1;
}
value = add(x,y);
if (-1 === value) {
value = 'N/A';
alert('Nelze sečíst');
}
//Exception
function add(a,b) {
if (a && b) {
return a + b;
}
else throw new Exception('Missing params');
}
try {
value = add(x,y);
} catch (Exception e) {
value = 0;
alert('Nelze sečíst');
}
Když porovnáte uvedené kódy, zjistíte, že se liší pouze tím, že místo „return -1;“ napíšete „throw new Exception()“ a místo „if (-1 === value) {}“ uvedete „try { } catch (Exception e) { }„, což je sice o pár znaků delší, ale na druhou stranu můžete funkci použít i pro záporné hodnoty (což u prvního kódu nelze) a v případě chyby se přímo dozvíte, k čemu došlo (místo nic neříkající -1).
A tohle je vše, co potřebujete o výjimkách vědět, abyste je mohli používat.
The Chosen One
Vyhodit a zachytit výjimku není problém, ale začne přituhovat, až narazíte na funkci, která hází několik různých výjimek a vy potřebujete poznat, kterou právě vyhodila.
function loginUser(username, password) {
if (!DB) { throw new Exception('Chybí DB'); }
user = DB->get('user', username);
if (user->isBanned) {
throw new Exception('User is banned');
}
if (!user->checkPassword(password)) {
throw new Exception('Invalid password');
}
return user->login();
}
Tahle funkce může vracet např. objekt třídy LoggedUser, ale zároveň vyhazuje celou řadu výjimek – a na každou bude potřeba reagovat jinak. Např. to, že se uživatel splete v hesle je celkem běžné a mělo by stačit zobrazit „Zkuste to znovu“, ale to, že se snaží přihlásit zabanovaný uživatel už je problém a bude potřeba např. skrýt tlačítko Registrovat, aby si nemohl udělat nový účet. A samozřejmě chybu databáze by bylo nejlépe nahlásit správci, aby zkontroloval, proč není dostupná.
Samozřejmě, že by šlo např. kontrolovat chybovou hlášku nebo v lepším případě číslo chyby…:
try { loginUser(form.username, form.password); }
catch (Exception e) {
if (e.getMessage() === 'Invalid password') {
alert('Zkuste to znovu!'); return;
}
if (e.code === 8) { //error DB conection
sendEmail('support@server.com', e);
}
else { ... }
}
… ale systém výjimek nám umožňuje vytvořit si vlastní výjimky a pak reagovat jen na určité typy.
class DbException extends Exception {
function contructor(code) {
this->code = code;
switch (code) {
case 1:
this->message = 'Network problem';
break;
case ...
}
}
}
Tuto vlastní výjimku pak vyhodíme stejně, jako obyčejnou výjimku, jen použijeme její kontruktor:
throw new DbException(8001); //ROOT user required
A když ji pak chceme zachytit, opět použijeme jméno třídy a tím řekneme, kterou výjimku zrovna chytáme.
try { loginUser(form.username, form.password); }
catch (DbException e) { //pro chyby databáze
sendEmail('support@server.com, e);
}
catch (Exception e) { //pro všechny ostatní
alert(e->getMessage());
}
Třídy výjimek je samozřejmě možné i řetězit a postupně dědit, ale vždy si zamyslete nad tím, zda potřebujete novou výjimku nebo jen stačí použít jiný kód chyby.
class InnoDbException extends DbException { }
class MyIsamDbException extends DbException { }
class InvalidPasswordForRootMyIsamDbException
extends MyIsamDbException { }
První dvě třídy dávají smysl, pokud potřebujete rozlišit, ve které databázi došlo k chybě (pokud máte správně rozlišené tabulky), ale třetí je zjevný nesmysl a úplně by postačilo pro chybné heslo vytvořit kód chyby.
V příkladu výše si všimněte ještě jedné věci – těla výjimek jsou záměrně prázdná (a není to proto, abych zbytečně do příkladu nevypisoval nepodstatný kód). Výjimky se totiž většinou dědí právě proto, aby je bylo možno rozlišit a není potřeba měnit jejich základní kód. Proto velice často narazíte právě na to, že se definuje nová výjimka, která má svůj kód poděděný od svého rodiče bez jediného rozdílu.
V JavaScriptu neexistují třídy ale pouze objekty a tak vlastní výjimka je obyčejný objekt se specifickými vlastnostmi. Vlastně jakýkoliv objekt lze vyhodit jako výjimku, akorát je otázka, jak se k tomu pak postaví kód, který ji zachytí.
//JavaScript
throw new Error("Chyba");
throw "Chyba";
throw {}; //prázdná výjimka bez zprávy a kódu
throw 10; //vyhodí kód jako "Error: 10"
throw { //kompletní chyba v JS
name: "DB error",
code: 123,
message: "Chyba připojení k DB.",
getMessage: function() {
return this.message;
},
toString: function(){
return this.name + this.code + ": "
+ this.message;
}
};
Jak už je v JavaScriptu zvykem, u chyby (obecně objektu) je nejdůležitější funkce toString(), která se použije v okamžiku, kdy bude potřeba chybu někam zobrazit (např. do konzole). Pro zachování kompatibility s ostatními jazyky a standardem používaným v současných prohlížečích pak doporučuji dát výjimce vlastnosti name (odpovídá jménu třídy výjimky), code (kód chyby) a message (vlastní zpráva). Dobrá je také funkce getMessage(), která bývá standardem v ostatních jazycích.
Pokud chcete JavaScriptovou chybu vytvořit tak, aby byla užitečná i pro debuggery, můžete jí přidat ještě vlastnosti fileName (jméno souboru), lineNumber (číslo řádky s chybou) a columnNumber (chybný znak v řádce). Pokud budou tyto vlastnosti popisovat existující řádek v načteném JS souboru, dokáže debugger pak přímo zobrazit kód, kde k chybě došlo, otevřít ho v editoru apod. Tohle může být ale užitečné jen v případě, že své JS soubory kompilujete a kompilátor bude moci do kódu vložit údaje pro konkrétní verzi souboru, podobně jako v PHP můžete použít např. __FILE__.
Samozřejmě pro výše uvedený objekt můžete vytvořit i konstruktor, jak je v JS zvykem:
//polyfill pro kompatibilitu s ostatními jazyky
window.Exception = function(message, code) {
this.message = message;
this.code = code;
this.name = 'Exception';
}
Exception.prototype = new Error('Exception');
throw new Exception('JS Výjimka');
//vypíše "Exception: JS Výjimka"
Probublávání
Stejně jako v HTML probublávají události, takže když kliknete do inputu, dozví se o tom postupně formulář, div, ve kterém je, body a nakonec i document, tak i výjimky probublávají přes volání funkcí (stack) až do tzv. hlavního handleru výjimek. A zatímco v HTML se nezachycená událost ignoruje, tak u výjimky naopak nezachycení zpravidla znamená pád celé programu – ve Windows známý hláškou „Program provedl nepovolenou operaci…“.
V JavaScriptu se tento globální handler definuje jako window.onerror (příp. v jQuery $(window).on(‚error‘, …); ), v PHP pomocí funkce set_error_handler().
Globální handler je ale až poslední záchrana (KPZ) a podstatné v předcházejícím odstavci je to, že výjimky probublávají, takže není potřeba zachytávat všechny hned na první úrovni.
function createUser(details) {
if (!DB) { throw new DbException() }
return DB->create('user', details);
}
function loginUser(username, password) {
if (!DB) { throw new DbException() }
user = DB->get('user', username);
if (!user->checkPassword(password)) {
throw new PasswordException();
}
return user->login();
}
function registerUser(details) {
createUser(details);
try {loginUser(details.name, details.pass);}
catch (PasswordException e) {
alert('Špatné heslo');
}
}
form.onsubmit = function() {
try { registerUser(this->getData()); }
catch (DbException e) {
alert('Chyba programu');
}
}
V příkladu je vidět, že výjimku s chybným heslem může vyhodit jen funkce loginUser() a tak ji také na daném místě odchytáváme a řešíme. Chybu s databází ale může vyhodit jak loginUser() tak i createUser() a proto ji nechytáme přímo, ale až ve funkci onsubmit() při volání registerUser().
Znovu-vyhození
Poslední možností, jak používat výjimky je tzv. znovu-vyhození (anglicky re-throw). Používá se v případech, kdy zjistíme, že výjimku nemůžeme ošetřit nebo když chceme výjimku změnit na jinou.
try {
copyProgress = filePosition / fileLength;
}
catch (Exception e) {
if (e.code === MathException.DivideByZero) {
throw new EmptyFileException();
} else {
throw e;
}
}
Zde reagujeme na výjimku a pokud je to dělení nulou, vyhodíme novou výjimku s tím, že je soubor prázdný, v ostatních případech znovu vyhodíme zachycenou výjimku – ta pak bude pokračovat v probublávání.
Pro začátečníky je u této praktiky spíše nutné vědět, že existuje, ale není potřeba se učit, jak ji správně používat.