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

4

5

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:

  • Don't set level and just assume the user is smart enough to work it out.
  • Wrap anything below your api entry points (api_function & another_api_fn) in a pcall, catch any error and re-bubble with a known "good" level value.
  • Don't ever error in lower api functions, always return nil, error or some similar pattern, then check for that in api_function and act as required.

My questions are:

  • Is it a problem to return the wrong level? It seems poor form to just "yeah whatever" a number in there and hope it's good.
  • If it is a problem, when is it ever a good practice to set the level (beyond maybe 0 which disables location reporting)
  • Which of the solutions, if any, are best practice? What should I actually do to write better maintainable code? Wrapping in pcall seems the easiest, since you can still rely on "normal errors" when testing and your functions are somewhat simpler, but somehow it feels like an antipattern, in my head.
Hello answered 17/11, 2020 at 8:8 Comment(0)
O
3

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.

Oath answered 17/11, 2020 at 15:59 Comment(0)
O
3

It is very uncommon in Lua to use error() to "bubble up" errors. It is much more common to return nil followed by a string describing the error, and leave it to the caller to decide whether the function failing is something it can recover from, whether it should try again, etc.

This is also more performant as it has less overhead than calling error() (It's just a normal function call)

Over-using pcall and forwarding errors is a very bad idea. It will just make your code much harder to follow.

Overland answered 17/11, 2020 at 8:56 Comment(3)
Would be helpful to know why this was disagreed with.Hello
@marksonedwardson I sure would love to know as well 🤔Overland
I'm the downvoter. I disagree with the phrase "forwarding errors [with pcall] is a very bad idea". I have tried to use pcall-less approach in one my project: every function returned nil, errmes in case of error, and after every function call I checked if returned value is nil to propagate the error further by return-ing the same nil, errmes. It was very boring. My code has grown up in size significantly. I will never follow pcall-less approach again. error+pcall is useful and necessary Lua feature. I disagree that pcall would make your code much harder to follow.Sclerotic
O
3

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.

Oath answered 17/11, 2020 at 15:59 Comment(0)
P
1

This seems like a case of a specific tool that exists for a specific purpose. From the documentation:

The error function has an additional second parameter, which gives the level where it should report the error; with it, you can blame someone else for the error. For instance, suppose you write a function and its first task is to check whether it was called correctly:

Then, someone calls your function with a wrong argument:

Lua points its finger to your function---after all, it was foo that called error---and not to the real culprit, the caller. To correct that, you inform error that the error you are reporting occurred on level 2 in the calling hierarchy (level 1 is your own function):

No language is perfect, and sometimes the designers will create a powerful and potentially dangerous tool in order to solve a specific problem. (C# reflection, for example) In this case, it seems like the intent is to report bad API calls in the function that made the call instead of the function that received it. So, levels 1 and 2 are the only intended levels as per the documentation, but that doesn't mean that other use cases don't exist.

You already highlighted the ways that (mis)using this language feature can cause more problems than it solves (aka. misleading the user with an error code that doesn't make sense). The question of good practice is often quite subjective, but our job is to write good, maintainable, bug free code regardless of the language we use. The designers of the language obviously thought that it was a good idea to put this tool in your toolbelt. You can start by reading the manual, but beyond that, it's up to you to understand the tool and use it well. If you don't feel like the tool should exist, or you just don't feel comfortable using it, then just stick with one of the alternative approaches you mentioned.

Postulant answered 17/11, 2020 at 16:39 Comment(0)
D
0

Is it a problem to return the wrong level? It seems poor form to just "yeah whatever" a number in there and hope it's good.

No, other than causing confusion. Error messages are there to help, and if you go "whatever" they won't. From the implementation's perspective there is no risk: luaB_error uses luaL_where to add the message with the error location. If you get too far, the information will be simply skipped:

function foo ()
   error("where am I", 7)
end
foo() -- lua: where am I
      -- stack traceback: ...

I have generally seen only levels 1 and 2, with latter usually used to indicate that the parameters passed to function were incorrect. But that's my observation with no particular data to support it.

As for the remaining questions, it's primarily opinion based. Use whatever fits you and your problem. Both error() and nil + message are reasonable approaches. You did great job researching it and seem to have a good enough understanding to decide.

Danidania answered 17/11, 2020 at 13:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.