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

  1. 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.
  2. 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.