8 Failures and faults
Builds on: Chapter 3 (or fail in signatures), chapter 6 (a failing push).
Questions this chapter answers
- What does Pinax do when something goes wrong? With no exceptions, how is it handled?
- Can a failure be passed over unhandled?
- What is a fault, and how does it differ from a failure?
8.1 Two kinds of wrong
Pinax tells two kinds of wrong apart.
- A failure is something expected now and then: text that is not a number, a full list, a missing key. A word that can fail has
or failin its signature, and the caller handles it right there. - A fault means the program is wrong: an overflow, a division by zero, an index out of range. The run stops on the spot, and the host receives it. A script has no way to catch a fault.
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.
| Name | When |
|---|---|
err.failed | a failure in general; the fault of a false assert |
err.not_found | something is missing: a key, a file, the end of the input, the separator of str.try_cut |
err.format | text or bytes of the wrong form: str.to_int, json, bytes.unpack |
err.full | a capacity is used up: a list, a queue, a buffer, the region |
err.range | an index outside the range; pop on an empty list, take on an empty queue |
err.overflow err.div_zero | an integer overflowed, a division by zero; faults, but the chk_ words give them as failures |
err.depth err.stale | calls too deep; a row or handle used after it was given back; faults |
err.cancelled err.deadlock err.interrupted | about 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:
or_fail— this word fails too, for the same reason; its signature needsor failas well;or_return— this word ends quietly;or_else … end— makes a value in its place;or_else (e) … endtakes the reason.
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
| Token | Stack after it | What 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 end | 0 | The 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
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
- After a word that can fail comes
or_fail,or_returnoror_else(standard 11.2). err NAMEdeclares a failure code of its module;err_textgives its name (8.14).- The body of
or_elseleaves, in place of the results of success, values of the same types (11.2). or_failandfailonly inside anor failsignature; outside definitions onlyor_else(11.2).- A fault stops the run, and the processor closes the handles the run opened (11.3).
- To receive overflow and division by zero as failures, use the
chk_words (15.1).