Към съдържанието
wannacode

Различни грешки и finally

Проблем

Сметката за нула души

Програмата дели сметката в ресторанта по равно. Вече знаеш за грешния вход и го хващаш:

bill.js
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("Въведи цяло число.");
}
}
Стъпка по стъпка, с думи
  1. Първо пускане: за дължината на линията е въведено „пет“.
  2. toInt не може да направи число от „пет“: хвърля се Error.
  3. Първата проверка е за RangeError. Не пасва, прескача се.
  4. Пасва else. Изпълнява се той и само той.
  5. Второ пускане: въведено е „-1“.
  6. toInt дава -1, а "-".repeat(-1) хвърля RangeError.
  7. Пасва още първата проверка. До else не се стига.
  8. Проверките вървят отгоре надолу. Изпълнява се първата, която пасва.
По-точно, за любопитните
  • Нещо, което не искаш да хванеш тук, хвърляш наново в else: throw e;. Как работи throw, е в следващия урок.

Идеята

Семейството на изключенията

Грешките са класове, свързани с наследяване. Ето най-честите:

Йерархията
Error
├── TypeError грешен вид стойност: undefined.seats
├── RangeError извън позволеното: "-".repeat(-1)
├── SyntaxError грешен запис: JSON.parse("{")
├── ReferenceError име, което го няма
├── URIError
├── EvalError
└── AggregateError

instanceof проверява посочения вид и всичките му наследници. Това е полиморфизмът от модула за наследяването: обект на наследника е и от вида на родителя.

const error = new RangeError("Invalid count value: -1");
console.log(error instanceof RangeError);
console.log(error instanceof Error);
console.log(error instanceof TypeError);
Изход
true
true
false

catch хваща всичко, а 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: -1
Error: "пет" не е цяло число.

e.name е името на вида на грешката: RangeError от repeat, Error от toInt.

По-точно, за любопитните
  • В JavaScript може да се хвърли всяка стойност, дори текст, затова в catch e не винаги е 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("Обща грешка");
}
}
Отговор

Пробвай

Намери грешката

При text = "-1" програмата печата „Нещо се обърка.“, а не „Дължината не може да е отрицателна.“. На кой ред е грешката?

Избери реда с грешката.

Идеята

Никой не задължава

В 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));
Отговор

Упражнение

Програми с различни грешки

Реши задачите в редактора си. Не печатай въпроси.

1. Цена на човек. Цените са в масив [120, 75, 300]. Прочети номер на продукт (от 1) и броя на хората и отпечатай цената на човек с целочислено деление. Несъществуващ продукт (undefined) и нула души провери с if, а грешния вход хвани и отпечатай съобщението му.

Вход Изход
2 и 3 На човек: 25 €
5 и 2 Няма такъв продукт.
1 и 0 Броят на хората не може да е 0.
1 и двама Грешка: "двама" не е цяло число.

2. Калкулатор. Прочети цяло число, оператор (+, -, * или /) и второ цяло число, всяко на свой ред. Отпечатай сметката (делението е Math.trunc(a / b)). При текст вместо число кажи „Въведи цели числа.“, при деление на нула „Деление на нула.“, а при друг оператор „Непознат оператор: “ и оператора. Делението на нула тук е if.

Вход
84
/
4
Изход
84 / 4 = 21
Вход
84
/
0
Изход
Деление на нула.

Още задачи

  • Терминалът. Функция pay(amountText) печата „Терминалът е свързан.“, после „Платени X €“ или съобщение за грешен вход, а във finally „Терминалът е изключен.“ Извикай го с „25“ и с „двайсет“.
  • Средна оценка без сривове. Прочети 5 реда с оценки. Пропусни тези, които не са цели числа (изключение) или не са от 2 до 6 (if). Отпечатай средното с целочислено деление. Ако нито една оценка не е вярна, отпечатай „Няма верни оценки.“ (тук делението на нула не хвърля, затова е if).
  • Предвиди и провери. Напиши try с catch и finally, в който всеки блок печата по ред. Предвиди изхода при успех, при хванато и при нехванато изключение и после го пусни.

Тест

Провери се

1. В catch има if / else if с instanceof. Изключението отговаря на две от проверките. Коя се изпълнява?
2. e е RangeError. Какво дава e instanceof Error?
3. Защо проверката e instanceof Error трябва да е последна?
4. Кога се изпълнява finally?
5. Колко catch блока може да има след един try в JavaScript?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·