When Things Go Wrong
Every program goes wrong sometimes, and every programmer spends a good part of their time finding out why. This chapter is about the two ways a TJS program goes wrong, how to read what it tells you, and how to handle mistakes on purpose.
Two kinds of wrong
- The program does not make sense, so it cannot run at all: a missing brace, a misspelled
word in the wrong place. The example shows an error with a line number instead of running.
Try it: delete the last
}in any example, read the message, then put the brace back. - The program runs, and a value is not what it should be: a word where a number belongs, a list with something missing. This chapter is mostly about these.
Errors are values
When a function is called with the wrong kind of value, TJS does not stop the program. The call gives back an error, a value that describes what went wrong:
function double(n: 0) {
return n * 2
}
const result = double('ten')
preview.append(result.message)
test 'a wrong value gives back an error' {
expect(result instanceof Error).toBe(true)
expect(result.message).toContain('Expected integer')
expect(result.received).toBe('ten')
}
Read the message from left to right: what was expected (an integer), where (the
function double, its parameter n), and what arrived (a string). Almost every mistake
you meet will be one of these, and the message says which part to look at.
The error also keeps what you passed, as result.received: here, 'ten'. When the problem is
a missing part, it keeps the whole thing, not the part. Give area from Things with Parts a
rectangle with no height, and the message names rect.height, but received is the
rectangle you gave it: the mistake is that it was not a whole rectangle.
Errors pass along
If you hand an error to another TJS function, it does not try to work with it: it gives the same error straight back. So the first mistake travels to the end, unchanged:
function double(n: 0) {
return n * 2
}
const first = double('ten')
const later = double(double(first))
preview.append(later.message)
test 'the first error reaches the end, unchanged' {
expect(later).toBe(first)
}
later is not just a similar error: it is the very same one (toBe checks that). Nothing
crashed, and the message is about the real mistake, 'ten', not about anything that happened
after it. That is the point: in a long chain of steps, you see where it started.
Checking for an error
When an error might come back, check for it before using the result, and decide what to show instead:
function double(n: 0) {
return n * 2
}
function describe(value: 0.0) {
const result = double(value)
if (result instanceof Error) {
return 'That is not a whole number.'
}
return `Double is ${result}.`
}
preview.append(describe(21), ' ', describe(2.5))
test 'describe handles both' {
expect(describe(21)).toBe('Double is 42.')
expect(describe(2.5)).toBe('That is not a whole number.')
}
describe takes any number (0.0), and double takes only whole numbers (0), so 2.5 gets
past the first check and is refused by the second. instanceof Error asks "is this an
error?", the same check as in Checking Your Work.
Making your own errors
Some mistakes are not about the kind of value but about what it means: dividing by zero, a date
in the future, an empty name. Your function can report those too, by giving back an Error with
a message of your own:
function divide(a: 0.0, b: 0.0) {
if (b == 0) {
return Error('cannot divide by zero')
}
return a / b
}
const answer = divide(1, 0)
preview.append(answer instanceof Error ? answer.message : `${answer}`)
test 'divide reports its own mistake' {
expect(divide(6, 3)).toBe(2)
expect(divide(1, 0) instanceof Error).toBe(true)
expect(divide(1, 0).message).toBe('cannot divide by zero')
}
Whoever calls divide checks for the error, as in the last section. Only the errors TJS
makes pass along by themselves: hand one of yours to a function that wants a number and it is
refused like any other wrong value, but the refusal keeps it, as received.
A good message says what went wrong in words the reader of the message can act on: "cannot divide by zero", not "error 7".
Looking inside a program
When a program gives a wrong answer and there is no error, look at what it is doing.
console.log prints any value as the program runs:
function average(numbers: [0.0]) {
let total = 0
for (const n of numbers) {
total = total + n
console.log('after', n, 'the total is', total)
}
return total / numbers.length
}
console.log('average:', average([2, 4, 9]))
Each line shows the program's state at one moment, so you can see where it stops matching what you expected. When you have found the mistake, write a test that would have caught it, then fix it: the test makes sure it stays fixed.
One more place to look: TJS remembers the recent mistakes it caught. At any example's console,
type __tjs.errors() to see them, newest last.
Try it
average([]) divides by zero and gives NaN, which means "not a number". Change average so
that an empty list gives back an Error saying so, and write the tests first: one for a normal
list, one for a list of one number, and one for the empty list.