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.