lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]

On 2015-01-15 8:21 PM, "Andrew Starks" <> wrote:
> On Thursday, January 15, 2015, Tim Hill <> wrote:
>>> On Jan 15, 2015, at 5:55 AM, Roberto Ierusalimschy <> wrote:
>>> The key point is: you cannot use a value to represent the absence of
>>> a value, at least not as a generic methodoly.
>>> So, if you need to return any value + error, you need more than one
>>> value (like 'pcall' does) or some other mechanism besides returns (like
>>> 'error'). That is a fact of life, and we are not going to solve it
>>> by throwing more values/non-values/semi-values/error-values into the
>>> language.
>> In one sense this is of course true by definition, but in another sense I think it’s just a case of moving the goal posts around. The fact is the concept of an “error” indication is an item of information. Whether this information is represented as an out-of-band data value (e.g. -1), the presence/absence of an additional return value, or the transfer of control to another location in the program (pcall) seems to me more a matter of style. In other words “i have an error because a value/type is xxx” and “i have an error because i reached this line of code” are really just different ways to express the same thing. From that standpoint I don’t see error() as any more (or less) logically fundamental that an explicit error type or value representation; they are both ultimately conventionalized ways to indicate the same thing.
>> It is of course true that one style might be perceived as more convenient or elegant than another, but that is a matter of taste and is probably why people get so passionate about error handing methodologies.
>> —Tim
> I think it might be useful to reserve the word "error" for "things that disrupt the stack" and something like "fail" for a return value that indicates the function's failure to return a result.
> Thinking about something in a different way has implications. If we stop conflating exceptional return values ;) by calling them "errors", it may help clarify their roll.
> open_file"foo"
> -->false, "NO 'foo'"
> read_next_chunk()
> -->nil, "TIMEOUT"
> Both of these make sense to me and would make less sense to me if they were reversed. Also, neither are errors in programming or in the system. As far as the context of the program is concerned, neither are errors, at all.
> -Andrew

One practice I've heard is to use return codes for runtime errors
(e.g. file not found) and exceptions for programming errors (e.g.
using a file after closing it). As they say, exceptions are for
exceptional circumstances (something went wrong and we can't really
deal with it here), not for control flow (an operation failed, which
is no big deal because we expected it might).

Personally I've sometimes wished Lua had try/catch[1] or some kind of
standard error/exception object, because I find the current method a
bit awkward:
-If you throw an error, then the caller needs to use pcall or xpcall
to catch it, which adds overhead (and ugliness with pcalls littered
everywhere) and especially is more complicated in C, and can make the
execution flow hard to follow.
-If you return (nil, errmsg), then the caller has to examine the error
message string to determine the issue, which is pretty ugly.

For example, here's a snippet from a project I'm working on (using Lua 5.1):
for i, path in ipairs(paths) do
    local file, err = loadfile(path)
    if file then
        foundConfig = true
        print("Loaded config file:", path)
        setfenv(file, config)
        local ok, err = pcall(file)
        if ok then break
        else error("Error parsing config file ("..path.."): "..tostring(err),0)
    elseif not err:find('No such file') then
        error("Error reading config file ("..path.."): "..tostring(err), 0)

This code looks in several possible locations for a config file. If
it's found, it attempts to compile and execute it[2] (with an
alternate environment) and then stops looking. It uses pcall() to
catch any runtime errors that might happen when executing the config
file, and reports them (in this case by re-throwing the error with an
explanatory message).

The trouble here is, loadfile() returns (nil, errormsg) for both
"failed to compile the file" and "the file doesn't exist". The only
way to tell which case occurred is to examine the error message.
However, if the error message is given in the user's native language,
then this method will fail on non-English systems, and if it's always
given in English, you're out of luck if you were hoping to display a
meaningful error message to a non-English-speaking user. Basically,
sometimes we need a human-readable error description, and sometimes we
need a machine-readable description, but we only get one or the other
(and can't even be sure which).

One method I've used in my own modules to work around this issue is to
return three values on error: (nil, errormessage, errorcode), which
usually means (nil, strerror(errno), errno)[3] or some equivalent.
Then the caller can examine the error code to determine what happened,
and also receives a human-readable message it can display if it
doesn't know what else to do. (Really, I'd return just the error code
and provide a function to map that to a human-readable message, but
then I'd be violating the "return (nil, message)" paradigm that many
Lua programs expect.) I use numeric error codes (with their names also
provided in the module, e.g. myModule.ERR_FILE_NOT_FOUND = 1), but you
could also use constant strings (e.g. "FILE_NOT_FOUND") for the
machine-readable error. The main thing is being consistent.

I personally think an even better paradigm would be for all errors,
whether returned or thrown, to be objects. You'd still do "return nil,
err" or "error(err)", but err would be a table with (at least) both a
human-readable error message and a machine-readable error code. (It
might also contain information such as the filename and line number
where the error occurred - which could be added automatically by
error() - and information specific to that error, such as which file
was not found.)

Granted, that doesn't solve the issue of assert(f()) where f() returns
false, but I think that's a separate problem. (To me it seems assert()
is overloaded, being used as both "throw an error if this value is
false, else return it" and "wrap this function and throw an error if
it fails".)

[1] Of course you can simulate it with pcall, but it's awkward and has
the overhead of creating a closure.
[2] I'm not concerned with security here.
[3] Actually I'd copy errno to some other variable first, because IIRC
reading it also resets it, but you get the point.

-- Sent from my Game Boy.