Vlastní chyby, rozšiřující chyba

Když něco vyvíjíme, často potřebujeme vlastní třídy chyb, které odrážejí konkrétní věci, které se mohou v našich úkolech pokazit. Pro chyby v síťových operacích můžeme potřebovat HttpError , pro databázové operace DbError , pro operace vyhledávání NotFoundError a tak dále.

Naše chyby by měly podporovat základní vlastnosti chyb, jako je message , name a nejlépe stack . Mohou však mít i jiné vlastní vlastnosti, např. HttpError objekty mohou mít statusCode vlastnost s hodnotou jako 404 nebo 403 nebo 500 .

JavaScript umožňuje použití throw s libovolným argumentem, takže technicky naše vlastní třídy chyb nemusí dědit z Error . Ale pokud dědíme, pak je možné použít obj instanceof Error k identifikaci chybných objektů. Takže je lepší z toho dědit.

Jak aplikace roste, naše vlastní chyby přirozeně tvoří hierarchii. Například HttpTimeoutError může dědit z HttpError , a tak dále.

Chyba rozšíření

Jako příklad uvažujme funkci readUser(json) který by měl číst JSON s uživatelskými daty.

Zde je příklad platného json může vypadat:

let json = `{ "name": "John", "age": 30 }`;

Interně budeme používat JSON.parse . Pokud obdrží chybné json , pak to vyhodí SyntaxError . Ale i když json je syntakticky správný, to neznamená, že je to platný uživatel, že? Mohou chybět potřebná data. Například nemusí mít name a age vlastnosti, které jsou pro naše uživatele zásadní.

Naše funkce readUser(json) nejen přečte JSON, ale zkontroluje (“ověří”) data. Pokud nejsou žádná povinná pole nebo je formát nesprávný, jedná se o chybu. A to není SyntaxError , protože data jsou syntakticky správná, ale jiný druh chyby. Budeme to nazývat ValidationError a vytvořit pro něj třídu. Chyba tohoto druhu by také měla obsahovat informace o problematickém poli.

Naše ValidationError třída by měla dědit z Error třída.

Error třída je vestavěná, ale zde je její přibližný kód, abychom pochopili, co rozšiřujeme:

// The "pseudocode" for the built-in Error class defined by JavaScript itself
class Error {
 constructor(message) {
 this.message = message;
 this.name = "Error"; // (different names for different built-in error classes)
 this.stack = <call stack>; // non-standard, but most environments support it
 }
}

Nyní zděděme ValidationError a vyzkoušejte to v akci:

class ValidationError extends Error {
 constructor(message) {
 super(message); // (1)
 this.name = "ValidationError"; // (2)
 }
}

function test() {
 throw new ValidationError("Whoops!");
}

try {
 test();
} catch(err) {
 alert(err.message); // Whoops!
 alert(err.name); // ValidationError
 alert(err.stack); // a list of nested calls with line numbers for each
}

Poznámka:v řádku (1) voláme nadřazený konstruktor. JavaScript vyžaduje, abychom zavolali super v podřízeném konstruktoru, takže je to povinné. Nadřazený konstruktor nastavuje message vlastnost.

Nadřazený konstruktor také nastavuje name vlastnost na "Error" , tedy v řádku (2) resetujeme na správnou hodnotu.

Zkusme to použít v readUser(json) :

class ValidationError extends Error {
 constructor(message) {
 super(message);
 this.name = "ValidationError";
 }
}

// Usage
function readUser(json) {
 let user = JSON.parse(json);

 if (!user.age) {
 throw new ValidationError("No field: age");
 }
 if (!user.name) {
 throw new ValidationError("No field: name");
 }

 return user;
}

// Working example with try..catch

try {
 let user = readUser('{ "age": 25 }');
} catch (err) {
 if (err instanceof ValidationError) {
 alert("Invalid data: " + err.message); // Invalid data: No field: name
 } else if (err instanceof SyntaxError) { // (*)
 alert("JSON Syntax Error: " + err.message);
 } else {
 throw err; // unknown error, rethrow it (**)
 }
}

try..catch blok ve výše uvedeném kódu zpracovává oba naše ValidationError a vestavěný SyntaxError z JSON.parse .

Podívejte se prosím, jak používáme instanceof pro kontrolu konkrétního typu chyby v řádku (*) .

Mohli bychom se také podívat na err.name , takto:

// ...
// instead of (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...

instanceof verze je mnohem lepší, protože v budoucnu se chystáme rozšířit ValidationError , vytvořte z něj podtypy, například PropertyRequiredError . A instanceof check bude nadále fungovat pro nové dědičné třídy. To je tedy zajištěno do budoucna.

Je také důležité, že pokud catch narazí na neznámou chybu, pak ji vrátí do řádku (**) . catch block pouze ví, jak zacházet s chybami ověření a syntaxe, jiné druhy (způsobené překlepem v kódu nebo jinými neznámými důvody) by měly propadnout.

Další dědictví

ValidationError třída je velmi obecná. Mnoho věcí se může pokazit. Tato vlastnost může chybět nebo může být ve špatném formátu (jako hodnota řetězce pro age místo čísla). Udělejme konkrétnější třídu PropertyRequiredError , přesně pro nepřítomné vlastnosti. Bude obsahovat další informace o vlastnosti, která chybí.

class ValidationError extends Error {
 constructor(message) {
 super(message);
 this.name = "ValidationError";
 }
}

class PropertyRequiredError extends ValidationError {
 constructor(property) {
 super("No property: " + property);
 this.name = "PropertyRequiredError";
 this.property = property;
 }
}

// Usage
function readUser(json) {
 let user = JSON.parse(json);

 if (!user.age) {
 throw new PropertyRequiredError("age");
 }
 if (!user.name) {
 throw new PropertyRequiredError("name");
 }

 return user;
}

// Working example with try..catch

try {
 let user = readUser('{ "age": 25 }');
} catch (err) {
 if (err instanceof ValidationError) {
 alert("Invalid data: " + err.message); // Invalid data: No property: name
 alert(err.name); // PropertyRequiredError
 alert(err.property); // name
 } else if (err instanceof SyntaxError) {
 alert("JSON Syntax Error: " + err.message);
 } else {
 throw err; // unknown error, rethrow it
 }
}

Nová třída PropertyRequiredError se snadno používá:potřebujeme pouze předat název vlastnosti:new PropertyRequiredError(property) . Lidsky čitelný message je generován konstruktorem.

Upozorňujeme, že this.name v PropertyRequiredError konstruktor je opět přiřazen ručně. To může být trochu únavné – přiřadit this.name = <class name> v každé vlastní třídě chyb. Můžeme se tomu vyhnout vytvořením vlastní třídy „základní chyby“, která přiřadí this.name = this.constructor.name . A pak z něj zdědit všechny naše vlastní chyby.

Říkejme tomu MyError .

Zde je kód s MyError a další vlastní třídy chyb, zjednodušeně:

class MyError extends Error {
 constructor(message) {
 super(message);
 this.name = this.constructor.name;
 }
}

class ValidationError extends MyError { }

class PropertyRequiredError extends ValidationError {
 constructor(property) {
 super("No property: " + property);
 this.property = property;
 }
}

// name is correct
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError

Vlastní chyby jsou nyní mnohem kratší, zejména ValidationError , protože jsme se zbavili "this.name = ..." řádek v konstruktoru.

Zabalení výjimek

Účel funkce readUser ve výše uvedeném kódu je „číst uživatelská data“. V procesu mohou nastat různé druhy chyb. Právě teď máme SyntaxError a ValidationError , ale v budoucnu readUser funkce může růst a pravděpodobně generovat další druhy chyb.

Kód, který volá readUser by měl zvládnout tyto chyby. Právě teď používá několik if s v catch blok, který zkontroluje třídu a zpracuje známé chyby a vrátí neznámé.

Schéma je takovéto:

try {
 ...
 readUser() // the potential error source
 ...
} catch (err) {
 if (err instanceof ValidationError) {
 // handle validation errors
 } else if (err instanceof SyntaxError) {
 // handle syntax errors
 } else {
 throw err; // unknown error, rethrow it
 }
}

Ve výše uvedeném kódu můžeme vidět dva typy chyb, ale může jich být více.

Pokud readUser funkce generuje několik druhů chyb, pak bychom si měli položit otázku:opravdu chceme pokaždé kontrolovat všechny typy chyb jednu po druhé?

Často je odpověď „Ne“:rádi bychom byli „o úroveň výše“. Chceme jen vědět, zda došlo k „chybě čtení dat“ – proč přesně k ní došlo, je často irelevantní (popisuje to chybové hlášení). Nebo, ještě lépe, rádi bychom měli způsob, jak získat podrobnosti o chybě, ale pouze v případě, že to potřebujeme.

Technika, kterou zde popisujeme, se nazývá „zabalení výjimek“.

  1. Vytvoříme novou třídu ReadError reprezentovat obecnou chybu „čtení dat“.
  2. Funkce readUser zachytí chyby při čtení dat, které se v něm vyskytují, například ValidationError a SyntaxError a vygenerujte ReadError místo toho.
  3. Číslo ReadError objekt si zachová odkaz na původní chybu ve svém cause vlastnictví.

Potom kód, který volá readUser bude muset zkontrolovat pouze ReadError , ne pro každý druh chyb čtení dat. A pokud potřebuje další podrobnosti o chybě, může zkontrolovat její cause vlastnost.

Zde je kód, který definuje ReadError a demonstruje jeho použití v readUser a try..catch :

class ReadError extends Error {
 constructor(message, cause) {
 super(message);
 this.cause = cause;
 this.name = 'ReadError';
 }
}

class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }

function validateUser(user) {
 if (!user.age) {
 throw new PropertyRequiredError("age");
 }

 if (!user.name) {
 throw new PropertyRequiredError("name");
 }
}

function readUser(json) {
 let user;

 try {
 user = JSON.parse(json);
 } catch (err) {
 if (err instanceof SyntaxError) {
 throw new ReadError("Syntax Error", err);
 } else {
 throw err;
 }
 }

 try {
 validateUser(user);
 } catch (err) {
 if (err instanceof ValidationError) {
 throw new ReadError("Validation Error", err);
 } else {
 throw err;
 }
 }

}

try {
 readUser('{bad json}');
} catch (e) {
 if (e instanceof ReadError) {
 alert(e);
 // Original error: SyntaxError: Unexpected token b in JSON at position 1
 alert("Original error: " + e.cause);
 } else {
 throw e;
 }
}

Ve výše uvedeném kódu readUser funguje přesně tak, jak je popsáno – zachytí chyby syntaxe a ověření a vyvolá ReadError místo toho chyby (neznámé chyby jsou znovu vyvolány jako obvykle).

Vnější kód tedy kontroluje instanceof ReadError a to je vše. Není třeba vypisovat všechny možné typy chyb.

Tento přístup se nazývá „zabalení výjimek“, protože bereme výjimky „nízké úrovně“ a „zabalíme“ je do ReadError to je abstraktnější. Je široce používán v objektově orientovaném programování.

Shrnutí

  • Můžeme dědit z Error a další vestavěné třídy chyb normálně. Jen se musíme postarat o name a nezapomeňte zavolat na číslo super .
  • Můžeme použít instanceof pro kontrolu konkrétních chyb. Funguje to i s dědičností. Někdy však máme chybový objekt pocházející z knihovny třetí strany a neexistuje snadný způsob, jak získat jeho třídu. Potom name vlastnost lze pro takové kontroly použít.
  • Zalamování výjimek je rozšířená technika:funkce zpracovává výjimky nízké úrovně a vytváří chyby vyšší úrovně namísto různých nízkoúrovňových. Nízkoúrovňové výjimky se někdy stávají vlastnostmi daného objektu jako err.cause ve výše uvedených příkladech, ale není to striktně vyžadováno.