When something goes wrong
Problem
12a instead of 12
The concert box office asks how many tickets you want. A ticket is €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("How many tickets? "));console.log(`Total: €${tickets * 15}`);rl.close();The first eight lines are ask() from the lesson on input. Below them is the program itself.
With the input 12 the program says "Total: €180". But the customer is in a hurry and hits one letter too many:
How many tickets? 12aTotal: €NaNThe program does not stop, but it is not right either: "€NaN" is not a total. Worse, it goes on as if everything were fine. How can the program tell that the input is wrong, and what should it do then?
The idea
Exception
Number has to return a number. It cannot make a number out of "12a", and it does not throw an error. It returns the special value NaN ("not a number"). NaN * 15 is still NaN. And parseInt is even quieter: it reads "12" and skips the rest.
console.log(Number("12a"));console.log(Number("12"));console.log(parseInt("12a"));NaN1212JavaScript often goes on with a wrong value instead of stopping. So the checking is up to you. Here it is in the ready-made function toInt (lines 10 to 16). For now, copy it; you will learn how to write it in the next lesson:
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}" is not a whole number.`); } return number;}
const tickets = toInt(await ask("How many tickets? "));console.log(`Total: €${tickets * 15}`);rl.close();When the text is not a whole number, toInt creates an object that describes the problem and throws it. This object is called an exception (in JavaScript, simply an error). If nobody catches it, the program stops:
How many tickets? 12afile:///projects/tickets/tickets.js:13 throw new Error(`"${text}" is not a whole number.`); ^
Error: "12a" is not a whole number. 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.2This text is called a stack trace, and you read it from top to bottom:
- At the very top: the file and the line where the exception was thrown, the line itself, and a
^under the spot. - The line after the empty one: the kind and the message.
Error: "12a" is not a whole number. - The lines with "at": where it happened. This is the call stack from the recursion lesson, from the inside out. At the top is
toInt, which threw, and under it is the place it was called from:tickets.js:18. The number after that is the column. The lines withnode:internalare in the code of Node.js, and you rarely need them.
The messages of the language itself are in English because the language writes them, not you.
Try it
Find the bug
The program should print the last score from the test. It stops with this:
/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.2Read the stack trace. Which line of the program is the bug on?
Pick the line with the bug.
scores[5] does not throw. It gives undefined, and the function returns it. The program stops only on line 6, where .toFixed(1) is called on undefined: TypeError: Cannot read properties of undefined. So in JavaScript the stack trace tells you where the error showed up, and sometimes the bug is earlier: where did this undefined come from? The last index is scores.length - 1.The idea
try and catch
An exception can be caught. The code that might throw goes in try. What should happen on an error, you write in catch:
const text = await ask("How many tickets? ");try { const tickets = toInt(text); console.log(`Total: €${tickets * 15}`);} catch (e) { console.log("That is not a whole number.");}console.log("Thank you!");12aHow many tickets? That is not a whole number.Thank you!- In the parentheses of
catchis the name of the exception, heree. You do not write a kind there:catchin JavaScript catches every error. How to tell the kinds apart is in the next lesson. - If everything in
trygoes well,catchis skipped. With the input12the output is "Total: €180" and "Thank you!". - After
catchthe program continues on. It no longer stops. - From here on we show only the code between
toIntandrl.close().
const text = await ask("How many tickets? ");try { const tickets = toInt(text); console.log(`Total: €${tickets * 15}`);} catch (e) { console.log("That is not a whole number.");}console.log("Thank you!");Step by step, in words
- The program asks how many tickets. You type "12a".
- In try: toInt tries to make a number out of "12a".
- It cannot. The function throws Error.
- Execution jumps straight into catch. The "Total" line is skipped.
- catch catches the error (in JavaScript, any error): it prints the message for the customer.
- After catch the program continues. It does not stop.
More precisely, if you are curious
- A variable declared in
try(heretickets) exists only until the closing brace oftry. If you need it after the block too, declare it beforetry.
- The name is optional:
catch { … }without parentheses works too, when you do not need the error.
Try it
Which lines run?
What will the program print?
console.log("Start");try { console.log("Reading"); const seats = toInt("ten"); console.log(`Got ${seats}`);} catch (e) { console.log("Bad input");}console.log("End");"Reading" is printed because it comes before the error. Then toInt throws, and execution jumps straight into catch. "Got" is skipped. After catch the program continues with "End".
The idea
What the exception says
The exception is an object, and e in catch (e) is the variable that points to it. The message is in e.message.
try { const tickets = toInt(await ask("")); console.log(`Total: €${tickets * 15}`);} catch (e) { console.log(`Error: ${e.message}`);}12aError: "12a" is not a whole number.Here the message is the one toInt writes. The errors of the language itself come with their own messages in English, and their kind is in 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')The message is useful to you when you are looking for the bug. To the customer you usually show your own text, in their language, such as "That is not a whole number."
The idea
Ask until it is right
The customer made a mistake. You do not have to stop. Ask again:
let tickets;while (true) { const text = await ask("How many tickets? "); try { tickets = toInt(text); break; } catch (e) { console.log("Enter a whole number, for example 3."); }}console.log(`Total: €${tickets * 15}`);12atwelve12How many tickets? Enter a whole number, for example 3.How many tickets? Enter a whole number, for example 3.How many tickets? Total: €180while (true) keeps looping until break stops it. And break is reached only if toInt succeeds. On an error, execution jumps into catch, and after it the loop starts over and asks again.
More precisely, if you are curious
Numbernever throws, so in JavaScript the usual check is withif:Number.isInteger(Number(text)). That is exactly whattoIntdoes inside. An exception is handy when the check is deep inside a function and the error has to reach the place that knows what to do.
Try it
Division by zero
Not every exception comes from the input. The average order value is the revenue divided by the number of orders. But on the first day there are no orders. What happens when you divide by zero?
What will the program print?
const revenue = 90;const orders = 0;const average = revenue / orders;console.log(average);Infinity. And 0 / 0 gives NaN. Even Math.trunc(90 / 0), the integer division from the lesson on operators, is still Infinity. The program goes on with a wrong value without saying anything. So before a division, check the divisor with if.More precisely, if you are curious
- In Java and C#, whole numbers have no value for infinity, so all that is left there is the exception. For decimal numbers, the rules come from the IEEE 754 standard for floating point numbers, which every language follows.
- In JavaScript all numbers are
number, decimal by IEEE 754, so division never throws an exception.
Exercise
Programs that do not stop
Solve these in your editor. Do not print prompts, so your output matches the sample.
1. Text to number. Write a function toNumber(text) modeled on toInt: it returns Number(text), and when the result is NaN (check with Number.isNaN) or the text is empty, it throws. Read two lines. For each, print the number or a message that it is not a number. Catch the exception where you call the function.
123.45123.abcThe number is: 123.45Error: 123.abc is not a number.2. No such product. The prices are in the array [120, 75, 300, 45, 89]. Read a product number from 1 to 5 and print its price. If the number is outside the array, say so. There is no exception here: prices[6] is undefined, so check with if. With the input 2 the output is Price: €75.
7There is no product number 7.3. Average order. Read the revenue and the number of orders (whole numbers) and print the average with integer division. If there are no orders, say so. There is no exception here either (Infinity), so check with if. With the input 900 and 4 the output is Average: €225.
900No orders yet.More tasks
- Age, until it is right. Ask for an age until the input is a whole number from 0 to 120. For text like "twenty" say "That is not a number.", and for 150 say "The age must be from 0 to 120." One is an exception, the other is an
if. - Sum of the valid ones. Read 5 lines. Add up the ones that are whole numbers and count the skipped ones: with the input
10,abc,5,7.5,3the output isSum: 18, skipped: 2. - Division on demand. Read two whole numbers and print the quotient. On text instead of a number and on division by zero, the program does not stop. It asks again for both numbers. Division by zero here is an
if.
Test