8 Failures and faults

Builds on: Chapter 3 (or fail in signatures), chapter 6 (a failing push).

Questions this chapter answers

8.1 Two kinds of wrong

Pinax tells two kinds of wrong apart.

With no exceptions, an error never skips several words and gets caught somewhere unexpected.

The reason of a failure is a value of type err. Twelve have names, and e err_text gives the name as text. Any of them can be raised with fail; some are also what the processor stops with as a fault.

NameWhen
err.faileda failure in general; the fault of a false assert
err.not_foundsomething is missing: a key, a file, the end of the input, the separator of str.try_cut
err.formattext or bytes of the wrong form: str.to_int, json, bytes.unpack
err.fulla capacity is used up: a list, a queue, a buffer, the region
err.rangean index outside the range; pop on an empty list, take on an empty queue
err.overflow err.div_zeroan integer overflowed, a division by zero; faults, but the chk_ words give them as failures
err.depth err.stalecalls too deep; a row or handle used after it was given back; faults
err.cancelled err.deadlock err.interruptedabout tasks (chapter 13) and the host (chapter 14)

8.2 Three words that handle a failure

Right after a word that can fail comes one guard:

08-parse.pnx

use io
use str
rem Read "name,score" lines; a bad line is reported, not fatal.
word parse (line str gives name str, score int or fail)
  line "," str.try_cut or_fail to (n, s)              rem no comma: fail the same way
  s str.trim str.to_int or_fail to v                  rem not a number: fail
  v 0 lt v 100 gt or if err.range fail end            rem our own rule: 0..100
  n str.trim v
end
word report (line str)
  line parse or_else (e) cat ("skipped \"", line, "\": ", e err_text) io.println return end to (name, score)
  cat (name, " scored ", score) io.println
end
text END
kim, 91
lee 77
park, ninety
choi, 140
END
str.lines do (l str) l report end each

Output

$ pnx 08-parse.pnx
kim scored 91
skipped "lee 77": not_found
skipped "park, ninety": format
skipped "choi, 140": range

parse can fail in three places and passes each on with or_fail. Its own rule (0 to 100) fails with err.range fail. report takes the failure with or_else (e), reports it with its reason, and skips the line.

08-or-return.pnx

use io
list seen int 3
word note_it (n int)
  n seen push or_return                              rem full: quietly do nothing
  cat ("noted ", n) io.println
end
1 note_it 2 note_it 3 note_it 4 note_it

Output

$ pnx 08-or-return.pnx
noted 1
noted 2
noted 3

After a guard the stack shall be the same on success and on failure. A word that failed leaves no results, so the body of or_else … end makes a value of the type success would have given, to take its place.

08-or-else-stack.pnx

use io
use str
word to_number (s str gives int)
  s str.to_int                        rem on success: one int on the stack
    or_else (e) 0 end                 rem on failure: the block gives one int instead
end                                   rem so after or_else the stack is the same either way
"42" to_number str.to_str io.println
"forty" to_number str.to_str io.println

Output

$ pnx 08-or-else-stack.pnx
42
0
TokenStack after itWhat happened
s"forty"The text goes on.
str.to_int—It does not read as a number and fails. No result.
or_else (e) 0 end0The way of the failure: the reason goes into e, and the body puts 0 on. On success the body is skipped and the number stays.

A body that leaves another type is refused: the code after or_else does not know which way it came (the same rule as for if in chapter 4).

08-or-else-wrong.pnx

use str
word to_number (s str gives int)
  s str.to_int or_else (e) "none" end  rem success gives an int, this block a str
end

Refused

$ pnx 08-or-else-wrong.pnx
08-or-else-wrong.pnx:3:35: the body of or_else must leave what the word would have given, or leave the word or the loop

08-guard-missing.pnx

use str
word twice (s str gives int) s str.to_int 2 mul end

Refused

$ pnx 08-guard-missing.pnx
08-guard-missing.pnx:2:43: a word that can fail is followed by or_fail, or_return or or_else

Common mistake: Using or_fail at the outermost level of a file

Code outside definitions counts as having no or fail signature, so neither or_fail nor or_return nor return can be used there. Put the work into a word, and take its failure with or_else at the outer level.

08-load-code-or-fail.pnx

use io
use str
"42" str.to_int or_fail str.to_str io.println

Refused

$ pnx 08-load-code-or-fail.pnx
08-load-code-or-fail.pnx:3:17: or_fail passes the failure on, so this word needs `or fail` in its signature

8.3 Faults

08-fault.pnx

use io
use str
word average (total int, n int gives int) total n div end
cat ("average of 10 over 4: ", 10 4 average) io.println
cat ("average of nothing: ", 0 0 average) io.println

Fault

$ pnx 08-fault.pnx
average of 10 over 4: 2
08-fault.pnx:3: division by zero
  total int = 0
  n int = 0

pnx shows the kind of the fault, its line and the locals of the word. What a run that ends with a fault made in the region is rewound, and handles such as files it opened are closed by the processor.

Where a fault could happen and you want a failure instead, use the chk_ words.

08-chk.pnx

use io
word safe_average (total int, n int gives int or fail) total n chk_div or_fail end
word show () cat ("10/4: ", 10 4 safe_average or_else (e) -1 end, ", 0/0: ", 0 0 safe_average or_else (e) -1 end) io.println end
show

Output

$ pnx 08-chk.pnx
10/4: 2, 0/0: -1

assert makes a fault when it is false. It writes down a promise the caller must keep.

08-assert.pnx

use io
word percent (part int, whole int gives int)
  whole 0 gt assert                                  rem a promise the caller must keep
  part 100 mul whole div
end
cat ("3 of 4: ", 3 4 percent, "%") io.println
cat ("3 of 0: ", 3 0 percent, "%") io.println

Fault

$ pnx 08-assert.pnx
3 of 4: 75%
08-assert.pnx:3: failed
  part int = 3
  whole int = 0

Q&A

Q. How do I decide what is a failure and what is a fault?

A. What happens because of data from outside (user input, files, the network) is a failure. What goes away only by fixing the program (a miscomputed index, a broken promise) is a fault.

Q&A

Q. My word has a new kind of failure to report. Can I make a new name?

A. Yes. err NAME declares a new failure code of its module. It is raised with fail, taken with or_else (e), compared with eq or by case, and err_text reads its name, as with the twelve.

08-own-codes.pnx

use io
use str
err too_short
err too_long
word check_password (p str or fail)
  p str.len 8 lt if too_short fail end
  p str.len 32 gt if too_long fail end
end
word try (p str)
  p check_password or_else (e) cat ("refused: ", e err_text) io.println return end
  "accepted" io.println
end
"abc" try
"a password of more than thirty-two bytes" try
"abcdefg8" try

Output

$ pnx 08-own-codes.pnx
refused: too_short
refused: too_long
accepted

When two reasons fail with one code, the caller cannot tell them apart: “too short” and “too long” would both be err.range. Declare a code for each reason. The host gets the name too (pinax_error_text). Codes are numbered from 0x400 in the order they are declared, so read the name, not the number.

Summary