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

Когато нещо се обърка

Проблем

12а вместо 12

Касата за концерта пита колко билета искаш. Билетът е 15 €.

tickets.js
import { createInterface } from "node:readline";
const rl = createInterface({ input: process.stdin });
const lines = rl[Symbol.asyncIterator]();
async function ask(question) {
process.stdout.write(question);
return (await lines.next()).value;
}
const tickets = Number(await ask("Колко билета? "));
console.log(`Общо: ${tickets * 15} €`);
rl.close();

Първите осем реда са ask() от урока за входа. Отдолу е самата програма.

С вход 12 програмата казва „Общо: 180 €“. Но клиентът бърза и натиска една буква повече:

Конзолата
Колко билета? 12а
Общо: NaN €

Програмата не спира, но и не е права: „NaN €“ не е сума. Още по-лошо, тя продължава, сякаш всичко е наред. Как програмата да разбере, че входът е грешен, и какво да прави тогава?

Идеята

Изключение

Number трябва да върне число. От „12а“ не може да направи число и не хвърля грешка, а връща специалната стойност NaN („не е число“). NaN * 15 пак е NaN. А parseInt е още по-тих: прочита „12“ и пропуска останалото.

console.log(Number("12а"));
console.log(Number("12"));
console.log(parseInt("12а"));
Изход
NaN
12
12

JavaScript често продължава с грешна стойност, вместо да спре. Затова проверката е твоя. Тук тя е в готовата функция toInt (редове 10–16). Засега я копирай; как се пише, ще разбереш в следващия урок:

tickets.js
import { createInterface } from "node:readline";
const rl = createInterface({ input: process.stdin });
const lines = rl[Symbol.asyncIterator]();
async function ask(question) {
process.stdout.write(question);
return (await lines.next()).value;
}
function toInt(text) {
const number = Number(text);
if (text.trim() === "" || !Number.isInteger(number)) {
throw new Error(`"${text}" не е цяло число.`);
}
return number;
}
const tickets = toInt(await ask("Колко билета? "));
console.log(`Общо: ${tickets * 15} €`);
rl.close();

Когато текстът не е цяло число, toInt създава обект, който описва проблема, и го хвърля. Този обект се казва изключение (exception, в JavaScript просто error). Ако никой не го хване, програмата спира:

Конзолата
Колко билета? 12а
file:///projects/tickets/tickets.js:13
throw new Error(`"${text}" не е цяло число.`);
^
Error: "12а" не е цяло число.
at toInt (file:///projects/tickets/tickets.js:13:11)
at file:///projects/tickets/tickets.js:18:17
at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
Node.js v22.22.2

Този текст се казва stack trace и се чете отгоре надолу:

  • Най-отгоре: файлът и редът, където е хвърлено изключението, самият ред и ^ под мястото.
  • Редът след празния: видът и съобщението. Error: "12а" не е цяло число.
  • Редовете с „at“: къде е станало. Това е стекът от извиквания от урока за рекурсията, отвътре навън. Най-горе е toInt, която е хвърлила, а под нея е мястото, откъдето е извикана: tickets.js:18. Числото след него е колоната. Редовете с node:internal са в кода на Node.js и рядко ти трябват.

Съобщенията на самия език са на английски, защото ги пише езикът, не ти.

Пробвай

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

Програмата трябва да отпечата последния резултат от контролното. Тя спира с това:

Грешка
/projects/tickets/scores.js:6
console.log(lastScore(scores).toFixed(1));
^
TypeError: Cannot read properties of undefined (reading 'toFixed')
at Object.<anonymous> (/projects/tickets/scores.js:6:30)
at Module._compile (node:internal/modules/cjs/loader:1705:14)
at Object..js (node:internal/modules/cjs/loader:1838:10)
at Module.load (node:internal/modules/cjs/loader:1441:32)
at Function._load (node:internal/modules/cjs/loader:1263:12)
at TracingChannel.traceSync (node:diagnostics_channel:328:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:237:24)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
at node:internal/main/run_main_module:36:49
Node.js v22.22.2

Прочети stack trace-а. На кой ред от програмата е грешката?

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

Идеята

try и catch

Изключението може да се хване. Кодът, който може да хвърли, отива в try („опитай“). Какво да стане при грешка, пишеш в catch („хвани“):

const text = await ask("Колко билета? ");
try {
const tickets = toInt(text);
console.log(`Общо: ${tickets * 15} €`);
} catch (e) {
console.log("Това не е цяло число.");
}
console.log("Благодарим ти!");
Вход
12а
Изход
Колко билета? Това не е цяло число.
Благодарим ти!
  • В скобите на catch е името на изключението, тук e. Вид там не се пише: catch в JavaScript хваща всяка грешка. Как се различават видовете, е в следващия урок.
  • Ако в try всичко мине, catch се прескача. С вход 12 изходът е „Общо: 180 €“ и „Благодарим ти!“.
  • След catch програмата продължава нататък. Тя вече не спира.
  • Тук и нататък показваме само кода между toInt и rl.close().

const text = await ask("Колко билета? ");
try {
const tickets = toInt(text);
console.log(`Общо: ${tickets * 15} €`);
} catch (e) {
console.log("Това не е цяло число.");
}
console.log("Благодарим ти!");
Стъпка по стъпка, с думи
  1. Програмата пита колко билета. Въвеждаш „12а“.
  2. В try: toInt опитва да направи число от „12а“.
  3. Не може. Функцията хвърля Error.
  4. Изпълнението скача направо в catch. Редът с „Общо“ се прескача.
  5. catch хваща грешката (в JavaScript всяка): печата съобщението за клиента.
  6. След catch програмата продължава. Не спира.
По-точно, за любопитните
  • Променлива, обявена в try (тук tickets), съществува само до затварящата скоба на try. Ако ти трябва и след блока, обяви я преди try.
  • Името не е задължително: catch { … } без скоби също работи, когато грешката не ти трябва.

Пробвай

Кое се изпълнява?

Какво ще отпечата програмата?

console.log("Начало");
try {
console.log("Чета");
const seats = toInt("десет");
console.log(`Прочетох ${seats}`);
} catch (e) {
console.log("Грешен вход");
}
console.log("Край");
Отговор

Идеята

Какво пише в изключението

Изключението е обект, а e в catch (e) е променливата, която сочи към него. Съобщението е в e.message.

try {
const tickets = toInt(await ask(""));
console.log(`Общо: ${tickets * 15} €`);
} catch (e) {
console.log(`Грешка: ${e.message}`);
}
Вход
12а
Изход
Грешка: "12а" не е цяло число.

Тук съобщението е на български, защото го пише toInt. Грешките на самия език са на английски, а видът им е в e.name:

try {
const scores = [62, 85, 47, 91, 78];
console.log(scores[5].toFixed(1));
} catch (e) {
console.log(e.name);
console.log(e.message);
}
Изход
TypeError
Cannot read properties of undefined (reading 'toFixed')

Съобщението е полезно за теб, когато търсиш грешката. На клиента обикновено показваш свой текст на неговия език, като „Това не е цяло число.“

Идеята

Питай, докато е вярно

Клиентът сбърка. Не е нужно да спираш. Питай отново:

let tickets;
while (true) {
const text = await ask("Колко билета? ");
try {
tickets = toInt(text);
break;
} catch (e) {
console.log("Въведи цяло число, например 3.");
}
}
console.log(`Общо: ${tickets * 15} €`);
Вход
12а
дванайсет
12
Изход
Колко билета? Въведи цяло число, например 3.
Колко билета? Въведи цяло число, например 3.
Колко билета? Общо: 180 €

while (true) се върти, докато break не го прекъсне. А break се стига само ако toInt е успяла. При грешка изпълнението скача в catch, след него цикълът започва отначало и пита пак.

По-точно, за любопитните
  • Number никога не хвърля, затова в JavaScript обичайната проверка е с if: Number.isInteger(Number(text)). Точно това прави и toInt отвътре. Изключението е удобно, когато проверката е дълбоко във функция, а грешката трябва да стигне до мястото, което знае какво да прави.

Пробвай

Деление на нула

Не всяко изключение идва от входа. Средната стойност на поръчка е приходът, делен на броя поръчки. Но в първия ден поръчки няма. Какво става при деление на нула?

Какво ще отпечата програмата?

const revenue = 90;
const orders = 0;
const average = revenue / orders;
console.log(average);
Отговор
По-точно, за любопитните
  • В Java и C# целите числа нямат стойност за безкрайност, затова там остава само изключението. При дробните числа правилата идват от стандарта за дробни числа IEEE 754, който всички езици следват.
  • В JavaScript всички числа са number, дробни по IEEE 754, затова изключение при деление няма никога.

Упражнение

Програми, които не спират

Реши задачите в редактора си. Не печатай въпроси, за да съвпадне изходът ти с примерния.

1. Текст към число. Напиши функция toNumber(text) по модела на toInt: тя връща Number(text), а когато резултатът е NaN (провери с Number.isNaN) или текстът е празен, хвърля. Прочети два реда. За всеки отпечатай числото или съобщение, че не е число. Хвани изключението там, където викаш функцията.

Вход
123.45
123.abc
Изход
Числото е: 123.45
Грешка: 123.abc не е число.

2. Няма такъв продукт. Цените са в масив [120, 75, 300, 45, 89]. Прочети номер на продукт от 1 до 5 и отпечатай цената му. Ако номерът е извън масива, кажи го. Тук изключение няма: prices[6] е undefined, затова провери с if. С вход 2 изходът е Цена: 75 €.

Вход
7
Изход
Няма продукт с номер 7.

3. Средна поръчка. Прочети прихода и броя поръчки (цели числа) и отпечатай средното с целочислено деление. Ако поръчки няма, кажи го. И тук изключение няма (Infinity), затова провери с if. С вход 900 и 4 изходът е Средно: 225 €.

Вход
90
0
Изход
Още няма поръчки.

Още задачи

  • Възраст, докато е вярна. Питай за възраст, докато входът не е цяло число от 0 до 120. За текст като „двайсет“ кажи „Това не е число.“, а за 150 кажи „Възрастта е от 0 до 120.“ Едното е изключение, другото е if.
  • Сума на верните. Прочети 5 реда. Събери тези, които са цели числа, и преброй пропуснатите: при вход 10, abc, 5, 7.5, 3 изходът е Сума: 18, пропуснати: 2.
  • Деление по желание. Прочети две цели числа и отпечатай частното. При текст вместо число и при деление на нула програмата не спира, а пита отново за двете числа. Делението на нула тук е if.

Тест

Провери се

1. Какво е изключение?
2. Какво дава Number("12а")?
3. В try има четири реда. Вторият хвърля изключение, а catch го хваща. Какво става с третия и четвъртия?
4. Какво дава 90 / orders, когато orders е 0?
5. Какво дава scores[10] в масив с 5 елемента?

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