Když nÄco vyvÃjÃme, Äasto potÅebujeme, aby naÅ¡e vlastnà chybové tÅÃdy odrážely specifické záležitosti, které se v naÅ¡ich úlohách mohou pokazit. Pro chyby v sÃÅ¥ových operacÃch můžeme potÅebovat tÅÃdu ChybaHttp, pro databázové operace ChybaDb, pro operace hledánà ChybaNenalezeno a tak dále.
NaÅ¡e chyby by mÄly podporovat základnà vlastnosti chyb jako message, name a pokud možno stack. Mohou vÅ¡ak mÃt i jiné, své vlastnà vlastnosti, napÅ. objekty tÅÃdy ChybaHttp mohou mÃt vlastnost stavovýKód s hodnotami jako 404 nebo 403 nebo 500.
JavaScript nám umožÅuje použÃvat throw s libovolným argumentem, takže technicky naÅ¡e vlastnà chybové tÅÃdy nemusejà dÄdit ze tÅÃdy Error. Pokud z nà vÅ¡ak dÄdÃme, bude možné k identifikaci chybových objektů použÃvat obj instanceof Error. Je tedy lepšà z nà dÄdit.
Když aplikace poroste, naÅ¡e vlastnà chyby budou tvoÅit pÅirozenou hierarchii. NapÅÃklad ChybaHttpTimeout může dÄdit z ChybaHttp a tak dále.
RozÅ¡iÅovánà tÅÃdy Error
Jako pÅÃklad uvažujme funkci naÄtiUživatele(json), která by mÄla naÄÃst JSON s uživatelskými daty.
Zde je pÅÃklad, jak může vypadat platný json:
let json = `{ "jméno": "Jan", "vÄk": 30 }`;
UvnitÅ použijeme JSON.parse. Jestliže obdržà poÅ¡kozený json, vyvolá SyntaxError. Ale i když je json syntakticky správnÄ, neznamená to, že obsahuje platného uživatele, že? Mohou v nÄm chybÄt nezbytná data. NapÅÃklad nemusà obsahovat vlastnosti jméno a vÄk, které jsou pro naÅ¡e uživatele nezbytné.
NaÅ¡e funkce naÄtiUživatele(json) tedy nebude jenom naÄÃtat JSON, ale také ovÄÅovat (âvalidovatâ) data. Pokud v nich nebudou požadovaná pole nebo formát nebude správný, nastane chyba. A nebude to SyntaxError, protože data jsou syntakticky správnÄ, ale chyba jiného druhu. Nazveme ji ChybaValidace a vytvoÅÃme pro ni tÅÃdu. Chyba tohoto druhu by mÄla obsahovat také informaci o vadném poli.
NaÅ¡e tÅÃda ChybaValidace by mÄla být zdÄdÄna ze tÅÃdy Error.
TÅÃda Error je vestavÄná, ale uvedeme jejà pÅibližný kód, abychom porozumÄli tomu, co rozÅ¡iÅujeme:
// âPseudokódâ pro vestavÄnou tÅÃdu Error definovanou samotným JavaScriptem
class Error {
constructor(zpráva) {
this.message = zpráva;
this.name = "Error"; // (různé názvy pro různé vestavÄné chybové tÅÃdy)
this.stack = <zásobnÃk volánÃ>; // nestandardnÃ, ale vÄtÅ¡ina prostÅedà jej podporuje
}
}
Nynà z nà zdÄÄme tÅÃdu ChybaValidace a vyzkouÅ¡ejme ji v akci:
class ChybaValidace extends Error {
constructor(zpráva) {
super(zpráva); // (1)
this.name = "ChybaValidace"; // (2)
}
}
function test() {
throw new ChybaValidace("Ouha!");
}
try {
test();
} catch(err) {
alert(err.message); // Ouha!
alert(err.name); // ChybaValidace
alert(err.stack); // seznam vnoÅených volánà s ÄÃslem Åádku u každého
}
ProsÃme vÅ¡imnÄte si: na Åádku (1) zavoláme rodiÄovský konstruktor. JavaScript vyžaduje, abychom v konstruktoru dÃtÄte volali super, takže to je povinné. RodiÄovský konstruktor nastavà vlastnost message.
RodiÄovský konstruktor také nastavà vlastnost name na "Error", takže ji na Åádku (2) pÅenastavÃme na správnou hodnotu.
Použijme chybu v metodÄ naÄtiUživatele(json):
class ChybaValidace extends Error {
constructor(zpráva) {
super(zpráva);
this.name = "ChybaValidace";
}
}
// PoužitÃ
function naÄtiUživatele(json) {
let uživatel = JSON.parse(json);
if (!uživatel.vÄk) {
throw new ChybaValidace("Chybà pole: vÄk");
}
if (!uživatel.name) {
throw new ChybaValidace("Chybà pole: jméno");
}
return uživatel;
}
// PÅÃklad fungovánà s try..catch
try {
let uživatel = naÄtiUživatele('{ "vÄk": 25 }');
} catch (chyba) {
if (chyba instanceof ChybaValidace) {
alert("Vadná data: " + chyba.message); // Vadná data: Chybà pole: jméno
} else if (chyba instanceof SyntaxError) { // (*)
alert("Syntaktická chyba JSONu: " + chyba.message);
} else {
throw chyba; // neznámá chyba, vyvoláme ji znovu (**)
}
}
V uvedeném kódu blok try..catch oÅ¡etÅuje jak naÅ¡i chybu ChybaValidace, tak vestavÄnou chybu SyntaxError z JSON.parse.
ProsÃme podÃvejte se, jak na Åádku (*) použÃváme instanceof k ovÄÅovánà specifického typu chyby.
Můžeme se podÃvat i do chyba.name, napÅÃklad:
// ...
// namÃsto (chyba instanceof SyntaxError)
} else if (chyba.name == "SyntaxError") { // (*)
// ...
Verze s instanceof je vÅ¡ak mnohem lepÅ¡Ã, protože v budoucnu se chystáme tÅÃdu ChybaValidace rozÅ¡iÅovat, vytváÅet jejà podtypy, napÅ. ChybaPožadovanéVlastnosti. A ovÄÅenà pomocà instanceof bude fungovat i pro novÄ zdÄdÄné tÅÃdy. Je tedy dopÅednÄ kompatibilnÃ.
Je také důležité, že pokud catch zachytà neznámou chybu, na Åádku (**) ji opÄtovnÄ vyvolá. Blok catch umà oÅ¡etÅit jen validaÄnà a syntaktické chyby, ostatnà druhy chyb (způsobené pÅeklepem v kódu nebo jinými neznámými pÅÃÄinami) by mÄly vypadnout výš.
Budoucà dÄdiÄnost
TÅÃda ChybaValidace je velmi obecná. Pokazit se může spousta vÄcÃ. Vlastnost může chybÄt nebo může být v nesprávném formátu (napÅÃklad ÅetÄzcová hodnota mÃsto ÄÃsla u vlastnosti vÄk). VytvoÅme tedy konkrétnÄjšà tÅÃdu ChybaPožadovanéVlastnosti, která bude sloužit výhradnÄ pro pÅÃpady chybÄjÃcÃch vlastnostÃ. Bude obsahovat dalšà informace o vlastnosti, která chybÃ.
class ChybaValidace extends Error {
constructor(zpráva) {
super(zpráva);
this.name = "ChybaValidace";
}
}
class ChybaPožadovanéVlastnosti extends ChybaValidace {
constructor(vlastnost) {
super("Chybà vlastnost: " + vlastnost);
this.name = "ChybaPožadovanéVlastnosti";
this.vlastnost = vlastnost;
}
}
// PoužitÃ
function naÄtiUživatele(json) {
let uživatel = JSON.parse(json);
if (!uživatel.vÄk) {
throw new ChybaPožadovanéVlastnosti("vÄk");
}
if (!uživatel.jméno) {
throw new ChybaPožadovanéVlastnosti("jméno");
}
return uživatel;
}
// PÅÃklad fungovánà s try..catch
try {
let uživatel = naÄtiUživatele('{ "vÄk": 25 }');
} catch (chyba) {
if (chyba instanceof ChybaValidace) {
alert("Vadná data: " + chyba.message); // Vadná data: Chybà vlastnost: jméno
alert(chyba.name); // ChybaPožadovanéVlastnosti
alert(chyba.vlastnost); // jméno
} else if (chyba instanceof SyntaxError) {
alert("Syntaktická chyba JSONu: " + chyba.message);
} else {
throw chyba; // neznámá chyba, vyvoláme ji znovu
}
}
PoužÃvánà nové tÅÃdy ChybaPožadovanéVlastnosti je snadné: postaÄà pÅedat název vlastnosti: new ChybaPožadovanéVlastnosti(vlastnost). Konstruktor vygeneruje ÄlovÄkem Äitelnou zprávu message.
ProsÃme vÅ¡imnÄte si, že this.name v konstruktoru ChybaPožadovanéVlastnosti se opÄt pÅiÅazuje ruÄnÄ. To může být ponÄkud úmorné â pÅiÅazovat this.name = <název tÅÃdy> v každé vlastnà chybové tÅÃdÄ. Můžeme se tomu vyhnout, když si vytvoÅÃme vlastnà âzákladnà chybovouâ tÅÃdu, která bude pÅiÅazovat this.name = this.constructor.name, a pak budeme vÅ¡echny naÅ¡e chyby dÄdit z nÃ.
NazvÄme ji MojeChyba.
Zde je kód s tÅÃdou MojeChyba a dalÅ¡Ãmi vlastnÃmi chybovými tÅÃdami, zjednoduÅ¡enÄ:
class MojeChyba extends Error {
constructor(zpráva) {
super(zpráva);
this.name = this.constructor.name;
}
}
class ChybaValidace extends MojeChyba { }
class ChybaPožadovanéVlastnosti extends ChybaValidace {
constructor(vlastnost) {
super("Chybà vlastnost: " + vlastnost);
this.vlastnost = vlastnost;
}
}
// název je správnÄ
alert( new ChybaPožadovanéVlastnosti("pole").name ); // ChybaPožadovanéVlastnosti
Nynà jsou naÅ¡e chyby, konkrétnÄ ChybaValidace, mnohem kratÅ¡Ã, jelikož jsme se zbavili Åádku "this.name = ..." v konstruktoru.
Obalovánà výjimek
ÃÄelem funkce naÄtiUživatele v uvedeném kódu je ânaÄÃst uživatelská dataâ. PÅi tomto procesu mohou nastat chyby různých druhů. MomentálnÄ máme SyntaxError a ChybaValidace, ale v budoucnu se funkce naÄtiUživatele může rozrůst a pravdÄpodobnÄ bude generovat chyby dalÅ¡Ãch druhů.
Tyto chyby by mÄl oÅ¡etÅovat kód, který funkci naÄtiUživatele volá. MomentálnÄ použÃvá v bloku catch nÄkolik pÅÃkazů if, které prozkoumajà tÅÃdu, oÅ¡etÅà známé chyby a opÄtovnÄ vyvolajà neznámé.
Schéma je následujÃcÃ:
try {
...
naÄtiUživatele() // potenciálnà zdroj chyb
...
} catch (chyba) {
if (chyba instanceof ChybaValidace) {
// oÅ¡etÅenà validaÄnÃch chyb
} else if (chyba instanceof SyntaxError) {
// oÅ¡etÅenà syntaktických chyb
} else {
throw chyba; // neznámá chyba, vyvoláme ji znovu
}
}
V uvedeném kódu vidÃme dva typy chyb, ale může jich tam být vÃc.
Jestliže funkce naÄtiUživatele generuje chyby nÄkolika druhů, mÄli bychom se zeptat sami sebe: opravdu chceme pokaždé ovÄÅovat vÅ¡echny typy chyb jeden po druhém?
OdpovÄÄ Äasto znà âneâ: chtÄli bychom být âo úroveÅ výš nad tÃm vÅ¡Ãmâ. Chceme vÄdÄt jen to, zda to byla âchyba naÄÃtánà datâ â proÄ pÅesnÄ se stala, je Äasto nepodstatné (popisuje to chybová zpráva). Nebo jeÅ¡tÄ lepšà by bylo, kdybychom mÄli způsob, jak zÃskat podrobnosti o chybÄ, ale jen když je budeme potÅebovat.
Technika, kterou tady popisujeme, se nazývá âobalovánà (wrapping) výjimekâ.
- VytvoÅÃme novou tÅÃdu
ChybaÄtenÃ, která bude pÅedstavovat obecnou chybu âÄtenà datâ. - Funkce
naÄtiUživatelebude zachytávat chyby naÄÃtánà dat, které nastanou uvnitÅ nÃ, napÅ.ChybaValidaceaSyntaxError, a mÃsto nich generovat chybuChybaÄtenÃ. - Objekt
ChybaÄtenÃsi ve své vlastnostipÅÃÄinabude udržovat odkaz na původnà chybu.
Pak kód, který volá funkci naÄtiUživatele, bude muset ovÄÅovat jen chybu ChybaÄtenÃ, ne každý druh chyby naÄÃtánà dat. A pokud bude potÅebovat podrobnosti o chybÄ, může se podÃvat na vlastnost pÅÃÄina.
Zde je kód, který definuje ChybaÄtenà a demonstruje jejà použità ve funkci naÄtiUživatele a try..catch:
class ChybaÄtenà extends Error {
constructor(zpráva, pÅÃÄina) {
super(zpráva);
this.pÅÃÄina = pÅÃÄina;
this.name = 'ChybaÄtenÃ';
}
}
class ChybaValidace extends Error { /*...*/ }
class ChybaPožadovanéVlastnosti extends ChybaValidace { /* ... */ }
function validujUživatele(uživatel) {
if (!uživatel.vÄk) {
throw new ChybaPožadovanéVlastnosti("vÄk");
}
if (!uživatel.jméno) {
throw new ChybaPožadovanéVlastnosti("jméno");
}
}
function naÄtiUživatele(json) {
let uživatel;
try {
uživatel = JSON.parse(json);
} catch (chyba) {
if (chyba instanceof SyntaxError) {
throw new ChybaÄtenÃ("Syntaktická chyba", chyba);
} else {
throw chyba;
}
}
try {
validujUživatele(uživatel);
} catch (chyba) {
if (chyba instanceof ChybaValidace) {
throw new ChybaÄtenÃ("Chyba validace", chyba);
} else {
throw chyba;
}
}
}
try {
naÄtiUživatele('{Å¡patný json}');
} catch (e) {
if (e instanceof ChybaÄtenÃ) {
alert(e);
// Původnà chyba: SyntaxError: Unexpected token š in JSON at position 1
alert("Původnà chyba: " + e.pÅÃÄina);
} else {
throw e;
}
}
V uvedeném kódu naÄtiUživatele funguje pÅesnÄ tak, jak jsme popsali â zachytává syntaktické a validaÄnà chyby a mÃsto nich vyvolává chyby ChybaÄtenà (neznámé chyby se vyvolávajà znovu jako obvykle).
VnÄjšà kód si tedy ovÄÅà instanceof ChybaÄtenà a to je vÅ¡e. Nenà tÅeba vyjmenovávat vÅ¡echny možné typy chyb.
Tento pÅÃstup se nazývá âobalovánà (wrapping) výjimekâ, protože pÅejÃmáme výjimky ânižšà úrovnÄâ a âobalujemeâ (anglicky âwrapâ) je do výjimky ChybaÄtenÃ, která je abstraktnÄjÅ¡Ã. V objektovÄ orientovaném programovánà se zeÅ¡iroka použÃvá.
ShrnutÃ
- Ze tÅÃdy
Errora jiných vestavÄných tÅÃd můžeme dÄdit bÄžným způsobem. Jen se musÃme postarat o vlastnostnamea nesmÃme zapomenout volatsuper. - K ovÄÅenà chyb urÄitého druhu můžeme použÃt
instanceof. Ten funguje i s dÄdiÄnostÃ. NÄkdy vÅ¡ak máme chybový objekt pocházejÃcà z knihovny tÅetà strany a neexistuje žádný snadný způsob, jak zÃskat jeho tÅÃdu. Pak můžeme pro taková ovÄÅenà použÃt vlastnostname. - Å iroce použÃvaná technika je obalovánà výjimek: funkce oÅ¡etÅà výjimky nižšà úrovnÄ a namÃsto různých chyb nižšà úrovnÄ vyvolá chybu vyššà úrovnÄ. Výjimky nižšà úrovnÄ se nÄkdy stávajà jejÃmi vlastnostmi, napÅÃklad
chyba.pÅÃÄinav uvedených pÅÃkladech, ale to nenà striktnÄ vyžadováno.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)