Когато нещо се обърка
Проблем
12а вместо 12
Касата за концерта пита колко билета искаш. Билетът е 15 €.
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а"));NaN1212JavaScript често продължава с грешна стойност, вместо да спре. Затова проверката е твоя. Тук тя е в готовата функция toInt (редове 10–16). Засега я копирай; как се пише, ще разбереш в следващия урок:
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:6console.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-а. На кой ред от програмата е грешката?
Избери реда с грешката.
scores[5] в JavaScript не хвърля, а дава undefined, и функцията го връща. Програмата спира едва на ред 6, където на undefined се вика .toFixed(1): TypeError: Cannot read properties of undefined. Затова в JavaScript stack trace-ът казва къде се е видяла грешката, а понякога тя е по-рано: откъде е дошло това undefined? Последният индекс е scores.length - 1.Идеята
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("Благодарим ти!");Стъпка по стъпка, с думи
- Програмата пита колко билета. Въвеждаш „12а“.
- В try: toInt опитва да направи число от „12а“.
- Не може. Функцията хвърля Error.
- Изпълнението скача направо в catch. Редът с „Общо“ се прескача.
- catch хваща грешката (в JavaScript всяка): печата съобщението за клиента.
- След catch програмата продължава. Не спира.
По-точно, за любопитните
- Променлива, обявена в
try(тукtickets), съществува само до затварящата скоба наtry. Ако ти трябва и след блока, обяви я предиtry.
- Името не е задължително:
catch { … }без скоби също работи, когато грешката не ти трябва.
Пробвай
Кое се изпълнява?
Какво ще отпечата програмата?
console.log("Начало");try { console.log("Чета"); const seats = toInt("десет"); console.log(`Прочетох ${seats}`);} catch (e) { console.log("Грешен вход");}console.log("Край");„Чета“ се отпечатва, защото е преди грешката. После toInt хвърля и изпълнението скача направо в catch. „Прочетох“ се пропуска. След catch програмата продължава с „Край“.
Идеята
Какво пише в изключението
Изключението е обект, а 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);}TypeErrorCannot 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);Infinity. А 0 / 0 дава NaN. И Math.trunc(90 / 0), целочисленото деление от урока за операторите, е пак Infinity. Програмата продължава с грешна стойност, без да каже нищо. Затова преди деление провери делителя с if.По-точно, за любопитните
- В Java и C# целите числа нямат стойност за безкрайност, затова там остава само изключението. При дробните числа правилата идват от стандарта за дробни числа IEEE 754, който всички езици следват.
- В JavaScript всички числа са
number, дробни по IEEE 754, затова изключение при деление няма никога.
Упражнение
Програми, които не спират
Реши задачите в редактора си. Не печатай въпроси, за да съвпадне изходът ти с примерния.
1. Текст към число. Напиши функция toNumber(text) по модела на toInt: тя връща Number(text), а когато резултатът е NaN (провери с Number.isNaN) или текстът е празен, хвърля. Прочети два реда. За всеки отпечатай числото или съобщение, че не е число. Хвани изключението там, където викаш функцията.
123.45123.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 €.
900Още няма поръчки.Още задачи
- Възраст, докато е вярна. Питай за възраст, докато входът не е цяло число от 0 до 120. За текст като „двайсет“ кажи „Това не е число.“, а за 150 кажи „Възрастта е от 0 до 120.“ Едното е изключение, другото е
if. - Сума на верните. Прочети 5 реда. Събери тези, които са цели числа, и преброй пропуснатите: при вход
10,abc,5,7.5,3изходът еСума: 18, пропуснати: 2. - Деление по желание. Прочети две цели числа и отпечатай частното. При текст вместо число и при деление на нула програмата не спира, а пита отново за двете числа. Делението на нула тук е
if.
Тест