Výjimky (ne)jen pro výjimečné

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.

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..