Allow pcall to receive any value type for error message

In most cases, when a non-string value is passed to the error function, a generic “error occurred” message is thrown. Presumably, this is because whatever code handles errors internally does not work well with anything except strings.

error({})
--> Error occurred, no output from Lua.

This also happens with the pcall function, despite error messages having no destination other than being a value returned by pcall. It even returns a different generic message (An error occurred vs Error occurred, no output from Lua.), suggesting that this is an assertion unique to pcall.

print(pcall(error, {}))
--> false An error occurred

When handling errors with pcall, it can be difficult to create and process structured errors with only a single string as a medium. This can be improved by allowing any type of value to be passed through error to pcall. Specifically tables, which can contain structured data.

local function HTTPRequest()
	error({message = "Not found", code = 404}, 2)
end

local ok, status = pcall(HTTPRequest)
if not ok then
	print(string.format("%d: %s", status.code, status.message))
end

For cases where an error is handled outside of pcall, and must be a string (e.g. logging, printing to output), there are several options:

  • Convert the value to a string, respecting the __tostring metamethod.
    • Allows structured errors to interface with the string-based system.
    • The security of the metamethod must be considered.
  • Convert the value to a string, without metamethods.
    • While not being as detailed, still provides a bit of information.
  • Continue to throw a generic error as usual.
    • Message fails normally, but succeeds when wrapped in pcall, which could be considered inconsistent.
7 Likes

This shouldn’t be hard; as far as I know, tostring() is already used internally.

tostring = nil
print('test') --> error

Kinda off-topic, but this is what actually happens (if set to nil in scope it just uses the default for that global):

> tostring = nil
print('test')
test

Unless they’ve recently changed it, it’s always been that setting tostring to nil would then error all functions that made use of it.

It’s also evident, although by sketchier means, that the function is pushed onto the stack of that function too

You can’t remove tostring from the environment like that in Roblox Lua though, because scripts’ environments inherit from the global table through a metatable __index field. If you set tostring to nil in the script environment (which it already is, actually) the access just falls back to the global table:

> print(rawget(getfenv(), "tostring"), tostring)
nil function: 264FDB8C
> tostring = nil; print(tostring)
function: 264FDB8C
> tostring = {}; print(tostring)
16:10:14.628 - attempt to call a table value
16:10:14.630 - Stack Begin
16:10:14.630 - Script 'tostring = {}; print(tostring)', Line 1
16:10:14.630 - Stack End

Woops, my bad.
Meant to say overwriting it not niling.
But yes my point was that it’s internally used.

In the case of what do when the error is not caught by pcall, I looked into how the standalone Lua interpreter handles this, and behaviour appears to vary wildly between the various versions of Lua:

Lua 5.1.5  Copyright (C) 1994-2012 Lua.org, PUC-Rio
> error(setmetatable({}, {__tostring = function() return "Hi" end}))
(error object is not a string)

Lua 5.2.4  Copyright (C) 1994-2015 Lua.org, PUC-Rio
> error(setmetatable({}, {__tostring = function() return "Hi" end}))
Hi
> error({})
(no error message)

Lua 5.3.4  Copyright (C) 1994-2017 Lua.org, PUC-Rio
> error(setmetatable({}, {__tostring = function() return "Hi" end}))
Hi
> error({})
(error object is a table value)
stack traceback:
        [C]: in function 'error'
        stdin:1: in main chunk
        [C]: in ?
> --(yes, this is the only case in which a traceback is emitted.)

I’d recommend doing what 5.3 does, that is,

(except we would want a stack trace in all cases).

Note that even in Roblox Lua, the xpcall function does allow any type of error value to be used:

> xpcall(function() error{} end, print)
table: 282CB01C

xpcall can be made to return the error value on failure like so:

> print(xpcall(function() error{} end, function(err) return err end))
false table: 282C52BC

This is a viable workaround if you don’t need the pcalled function to be able to yield.

I’m going to guess the reason pcall behaves differently is because Roblox rewrote it to handle yields.

Yielding xpcall was going to be my next request, but it seems like there are already enough of those.

1 Like

A recent update (presumably 425) stealthily implemented this. Rerunning the example now produces the following output:

print(pcall(error, {}))
--> false table: 0x0123456789abcdef

An unprotected error still produces the “Error occurred” message as before:

error({})
--> Error occurred, no output from Lua.

xpcall continues to behave as before, with the addition that it can now yield as pcall does.

6 Likes