"It's impossible to chain multivalue functions together to write anything succinctly"
Lua's standard error return idiom is: nil/false, error-string, error-code. The Lua standard library also includes a function, assert, which will throw an error if the first argument is nil/false. The value thrown is the second return value, which in the idiomatic case is the error string. If the first argument is not nil/false, then assert returns the entire list of values.
Thus _you_ can choose the best approach based on _your_ needs. The following are _both_ examples of idiomatic Lua.
Example 1:
local v, errdesc, errcode = foo()
if not v then
if errcode == 1 then
...
elseif errcode == 2 then
...
else
error(errdesc)
end
end
Example 2:
local v = assert(foo())
Example 2 (easy composition):
local v = assert(foo(assert(bar())))
You're not limited to that. Neither assert nor error are specialized built-ins. You could throw a structured object with, optionally, a __tostring metamethod for string coercion; or you could write your own routine that manipulated the values differently. But in idiomatic Lua it's not common for thrown errors to be caught and differentiated, and code usually expects the thrown value to be a string or coercible to a string. If it's thrown it's usually because there's nothing particular that can be done, and so usually it's caught at the boundary of some logical transaction. But Lua allows you put that boundary anywhere, and you're not limited to that idiom. In some cases you want more structured exceptions, and you can have them. But as a module author the expectation is that you preserve the application's freedom of choice, so modules usually return the idiomatic tuple whether or not it's recoverable.
There are some errors that Lua will always throw, like memory allocation failures. These can be caught like any error, and the Lua VM is never put in an inconsistent state. But they're an example of the kind of non-specific error where you normally only catch them, if at all, at some logical transactional boundary--an image transformation in a photo editor, or a client request for a web server. Lua is, notably, one of the few scripting languages where allocation failure is recoverable. And rather trivially recoverable, in fact.
EDIT: error() is a specialized built-in: it's how you "throw" in Lua. But it will throw any value, not just a string.