Classic Icon traditionally uses failure to indicate that a particular operation has not succeeded. For example, the builtin open
function will fail if, for some reason, the given file cannot be opened. Unfortunately, this leaves the caller guessing as to why the operation didn’t succeed.
Object Icon uses a very simple way to improve things. A keyword &why
is provided, which is really just a simple global string variable. A procedure or function performing an operation can, on failure, set this keyword to a string indicating the reason for failure. For example, consider the following interaction in ieval
:-
$ ieval -i io
> open("nonsense.txt")
> &why
"No such file or directory (errno=2)"
> open("/root/nonsense.txt")
> &why
"Permission denied (errno=13)"
>
&why
can be assigned to like any other variable. It is helpful when propagating errors, to keep any existing value of &why
, and prepend a message, usually separated with a colon and a space (“:”). For example :-
procedure open_database(x)
...
(f := open(x || ".db")) | {
&why := "Failed to open database: " || &why
fail
}
...
end
Since assignment to &why
is almost always followed by failure, there is a helpful utility procedure, util.error
, which simply sets &why
and fails. This would make the above code a little more concise :-
procedure open_database(x)
...
(f := open(x || ".db")) | return error("Failed to open database: " || &why)
...
end
Another useful library procedure is ipl.printf.whyf
. This takes a printf-style format and parameters and sets &why
to the resulting string. The format “%w” can be used to insert &why
into the result, so the above example using error()
could be written as :-
return whyf("Failed to open database: %w")
C programmers will notice that &why
is very similar to errno
, the global variable by which various C library functions communicate error information. Indeed, in the ieval
output given above, the values for &why
come from turning errno
into a string form. When errno
is used in this way by Object Icon, the raw value of errno
is retained by appending its value to the end of the string; thus rather than simply “Permission denied”, we set &why
to “Permission denied (errno=13)”. This is occasionally useful when we wish to do something if failure occurred for a specific reason. To extract the raw errno
value from &why, the utility procedure util.errno
may be used, as follows :-
$ ieval -i io,util
> open("/root/nonsense.txt")
> &why
"Permission denied (errno=13)"
> errno()
13
>
The various constant values which errno
may take can be found in the class posix.Errno
. We could then use logic like :-
(f := open(s)) | {
if errno() = Errno.EACCES then
# Do something if permission denied (EACCES is 13).
else
# Any other reason
}
One point to note is that the presence of these constants is somewhat system dependent. Plan 9 for example, doesn’t use errno
at all and uses a string-based error message system.
Occasionally it is useful to call a procedure, but to prevent it from altering the value of &why
. This is easily achieved with the utility procedure util.save_why
, which can be used as follows :-
# &why has some important value...
save_why{Files.remove(tmp_file)}
# &why still has the same value, regardless of whether `remove` failed.
save_why
makes use of the technique described on this page.