error-handlinglualua-5.1

Is using lua's error(.., level) an anti pattern?


Lua 5.1's API provides an error() function, which accepts a string (the error message) and a "level".

My understanding is that level, lets you move up the call stack, so you can provide nicer error reporting, especially when delivering a module as an API.

For example, imagine the user calls api_function(x) with x = nil. This will be an error, but the API doesn't know until it's quite a bit into it's code.

It may result in this call stack:

api_function(x)                     : user_file.lua:30
  -> api_function                   : api.lua:20
    -> some_function                : api.lua:250
      -> handle_when_x_string       : api_string.lua:20
        -> error("value is nil")    : api_string.lua:66

As written, the user will see something like api_string.lua:66 error: value is nil, when what they really want to see the "nice" error, user_file.lua:30 error: value is nil. ("Is that error my fault or a bug in the API?")

Now, we can change the code to "pop the call stack",

api_function(x)                     : user_file.lua:30
  -> api_function                   : api.lua:20
    -> some_function                : api.lua:250
      -> handle_when_x_string       : api_string.lua:20
        -> error("value is nil", 5) : api_string.lua:66

Which will return the "nice" error, but, imagine you can also call handle_when_x_string more directly (poor API design aside),

another_api_fn(x)                     : user_file.lua:44
  -> another_api_fn                   : api.lua:11
    -> handle_when_x_string           : api_string.lua:20
      -> error("value is nil", 5)     : api_string.lua:66

Now our "pop level" is incorrect. Perhaps in this example, it would simply pop to the top and stop trying, but the principle of "incorrect level" remains at least uncomfortable, it may even pop "out" of where the user caused the error.

I can see a few solutions:

My questions are:


Solution

  • First, you need to differentiate from errors due to a bad API call, and actual bugs in your code.

    If the purpose of the error call is to tell the API user that they passed the wrong arguments, you should validate the arguments in every API function, so that the error level will be knowable, and so the rest of your library knows it's working with valid arguments. If you end up with a complicated hierarchy of validating functions, they can take parameters for the function name and error level. Here's a very contrived example for how you can use error levels:

    local function lessThan100(x, funcName, errorLevel)
      if x >=100 then
        error(funcName .. ' needs a number less than 100', errorLevel)
      end
    end
    
    local function numLessThan100(x, funcName, errorLevel)
      if type(x) ~= 'number' then
        error(funcName .. ' needs a number', errorLevel)
      end
      lessThan100(x, funcName, errorLevel + 1)
    end
    
    -- API function
    local function printNum(x)
      numLessThan100(x, 'printNum', 3)
      print(x)
    end
    

    If the error call represents a bug in your code, then don't use a level, because you can't know what triggers the bug.