Различни грешки и finally
Проблем
Сметката за нула души
Програмата дели сметката в ресторанта по равно. Вече знаеш за грешния вход и го хващаш:
try { const bill = toInt(await ask("")); const people = toInt(await ask("")); console.log(`На човек: ${Math.trunc(bill / people)} €`);} catch (e) { console.log("Въведи цели числа.");}Сметката е 90 €, а някой въвежда 0 за броя на хората:
На човек: Infinity €Програмата не спира, но пише глупост. Делението на нула в JavaScript не хвърля, а дава Infinity, затова catch няма какво да хване. А когато грешките наистина са от различен вид, catch пак е само един. Как тогава да кажеш на клиента точно какво не е наред?
Идеята
Един catch, няколко вида
Нулата не е изключение, затова тя е if:
try { const bill = toInt(await ask("")); const people = toInt(await ask("")); if (people === 0) { console.log("Хората не може да са 0."); } else { console.log(`На човек: ${Math.trunc(bill / people)} €`); }} catch (e) { console.log("Въведи цели числа.");}| Вход | Изход |
|---|---|
| 90 и 3 | На човек: 30 € |
| 90 и 0 | Хората не може да са 0. |
| 90 и трима | Въведи цели числа. |
Но има грешки, които JavaScript хвърля сам. Бележката има линия от тирета, а дължината ѝ се въвежда. "-".repeat(-1) хвърля RangeError, а toInt хвърля Error. Два вида, а catch след try е само един. Видът се проверява вътре в него с instanceof („е от вида“):
const text = await ask("");try { const width = toInt(text); console.log("-".repeat(width));} catch (e) { if (e instanceof RangeError) { console.log("Дължината не може да е отрицателна."); } else { console.log("Въведи цяло число."); }}| Вход | Изход |
|---|---|
| 5 | ----- |
| -1 | Дължината не може да е отрицателна. |
| пет | Въведи цяло число. |
Проверките вървят отгоре надолу, като всеки if / else if. Изпълнява се първият клон, чийто вид отговаря, и само той.
const text = await ask("");try { const width = toInt(text); console.log("-".repeat(width));} catch (e) { if (e instanceof RangeError) { console.log("Дължината не може да е отрицателна."); } else { console.log("Въведи цяло число."); }}Стъпка по стъпка, с думи
- Първо пускане: за дължината на линията е въведено „пет“.
- toInt не може да направи число от „пет“: хвърля се Error.
- Първата проверка е за RangeError. Не пасва, прескача се.
- Пасва else. Изпълнява се той и само той.
- Второ пускане: въведено е „-1“.
- toInt дава -1, а "-".repeat(-1) хвърля RangeError.
- Пасва още първата проверка. До else не се стига.
- Проверките вървят отгоре надолу. Изпълнява се първата, която пасва.
По-точно, за любопитните
- Нещо, което не искаш да хванеш тук, хвърляш наново в
else:throw e;. Как работиthrow, е в следващия урок.
Идеята
Семейството на изключенията
Грешките са класове, свързани с наследяване. Ето най-честите:
Error├── TypeError грешен вид стойност: undefined.seats├── RangeError извън позволеното: "-".repeat(-1)├── SyntaxError грешен запис: JSON.parse("{")├── ReferenceError име, което го няма├── URIError├── EvalError└── AggregateErrorinstanceof проверява посочения вид и всичките му наследници. Това е полиморфизмът от модула за наследяването: обект на наследника е и от вида на родителя.
const error = new RangeError("Invalid count value: -1");console.log(error instanceof RangeError);console.log(error instanceof Error);console.log(error instanceof TypeError);truetruefalsecatch хваща всичко, а e.name казва вида:
function printLine(widthText) { try { console.log("-".repeat(toInt(widthText))); } catch (e) { console.log(`${e.name}: ${e.message}`); }}
printLine("5");printLine("-1");printLine("пет");-----RangeError: Invalid count value: -1Error: "пет" не е цяло число.e.name е името на вида на грешката: RangeError от repeat, Error от toInt.
По-точно, за любопитните
- В JavaScript може да се хвърли всяка стойност, дори текст, затова в
catcheне винаги еError. Ще го видиш в следващия урок.
- Хващай толкова конкретно, колкото можеш. catch без проверка на вида скрива и грешки, които не си очаквал, например бъг в собствения ти код.
Пробвай
Кой catch?
Какво ще отпечата програмата?
const tables = [{ seats: 4 }, { seats: 2 }, { seats: 6 }];try { console.log(`Маса 4: ${tables[3].seats} места`);} catch (e) { if (e instanceof RangeError) { console.log("Извън позволеното"); } else if (e instanceof TypeError) { console.log("Грешна стойност"); } else { console.log("Обща грешка"); }}tables[3] не хвърля, а е undefined. Грешката идва при .seats: от undefined не може да се чете, и това е TypeError. Тя не е RangeError, затова първата проверка не минава. Втората минава и се изпълнява. До else не се стига, макар че и той би хванал.Пробвай
Намери грешката
text = "-1" програмата печата „Нещо се обърка.“, а не „Дължината не може да е отрицателна.“. На кой ред е грешката?Избери реда с грешката.
RangeError е наследник на Error, затова e instanceof Error е вярно и за нея. Първата проверка хваща всичко, а втората никога не може да се стигне. JavaScript не казва нищо: програмата върви и просто печата общото съобщение. Правилото е от конкретното към общото: размени двете проверки.Идеята
Никой не задължава
В Java има проверени изключения: компилаторът не приема програмата, докато не ги хванеш или не ги обявиш с throws. В JavaScript такива няма. Нито една грешка не е задължителна за хващане:
function printReceipt() { console.log("Печатам бележката..."); console.log("-".repeat(20)); console.log("Готово.");}
printReceipt();Печатам бележката...--------------------Готово.repeat хвърля RangeError при отрицателна дължина, но никой не иска да го хванеш. Същото е за toInt.
Затова отговорността е твоя. Кои грешки хвърля една функция, пише в документацията ѝ. В MDN, справочника за JavaScript, това е разделът „Exceptions“. Прочети го, преди да решиш дали да хващаш.
Идеята
finally: винаги
Принтерът на касата трябва да се освободи след всяка бележка, иначе следващата няма да се отпечата. Каквото и да стане, успех или грешка:
function printReceipt(qtyText) { console.log("Принтерът е зает."); try { const qty = toInt(qtyText); console.log(`Кафе: ${qty} x 2 € = ${qty * 2} €`); } catch (e) { console.log(`Грешно количество: ${qtyText}`); } finally { console.log("Принтерът е свободен."); }}
printReceipt("3");printReceipt("три");Принтерът е зает.Кафе: 3 x 2 € = 6 €Принтерът е свободен.Принтерът е зает.Грешно количество: триПринтерът е свободен.finally се изпълнява винаги: когато всичко мине, когато изключението е хванато и дори когато catch го няма или хвърля наново. В последния случай finally се изпълнява, а изключението продължава нагоре към следващия try, който може да го хване.
Защо не просто ред след try/catch? Защото той се пропуска, ако изключението не е хванато, или ако в try има return. finally не се пропуска.
По-точно, за любопитните
- Може и
tryсfinally, безcatch: изключението не се хваща, но почистването се изпълнява.
- В Java и Python има по-кратък запис, който сам затваря файл или връзка. В Node.js 22 такъв няма (
usingидва в по-нови версии и тук даваSyntaxError), затова там почистването е въвfinally.
finallyне се изпълнява само ако програмата бъде спряна изцяло, например сprocess.exit(0).
Пробвай
finally и return
Какво ще отпечата програмата?
function reserve(guests) { try { if (guests > 6) { return `Няма маса за ${guests}`; } return `Маса за ${guests}`; } finally { console.log("Записах опита."); }}
console.log(reserve(8));return в try не прескача finally. Функцията първо изпълнява finally, после връща текста. Затова „Записах опита.“ се отпечатва преди резултата, който console.log получава чак когато функцията свърши.
Упражнение
Програми с различни грешки
Реши задачите в редактора си. Не печатай въпроси.
1. Цена на човек. Цените са в масив [120, 75, 300]. Прочети номер на продукт (от 1) и броя на хората и отпечатай цената на човек с целочислено деление. Несъществуващ продукт (undefined) и нула души провери с if, а грешния вход хвани и отпечатай съобщението му.
| Вход | Изход |
|---|---|
| 2 и 3 | На човек: 25 € |
| 5 и 2 | Няма такъв продукт. |
| 1 и 0 | Броят на хората не може да е 0. |
| 1 и двама | Грешка: "двама" не е цяло число. |
2. Калкулатор. Прочети цяло число, оператор (+, -, * или /) и второ цяло число, всяко на свой ред. Отпечатай сметката (делението е Math.trunc(a / b)). При текст вместо число кажи „Въведи цели числа.“, при деление на нула „Деление на нула.“, а при друг оператор „Непознат оператор: “ и оператора. Делението на нула тук е if.
84/484 / 4 = 2184/0Деление на нула.Още задачи
- Терминалът. Функция
pay(amountText)печата „Терминалът е свързан.“, после „Платени X €“ или съобщение за грешен вход, а въвfinally„Терминалът е изключен.“ Извикай го с „25“ и с „двайсет“. - Средна оценка без сривове. Прочети 5 реда с оценки. Пропусни тези, които не са цели числа (изключение) или не са от 2 до 6 (
if). Отпечатай средното с целочислено деление. Ако нито една оценка не е вярна, отпечатай „Няма верни оценки.“ (тук делението на нула не хвърля, затова еif). - Предвиди и провери. Напиши
tryсcatchиfinally, в който всеки блок печата по ред. Предвиди изхода при успех, при хванато и при нехванато изключение и после го пусни.
Тест