More context for non-error output messages

As a Roblox developer, it is currently too hard to track where normal output messages come from.

The current approach for implementing something like this would be to use LogService.MessageOut and ScriptContext.Error, but the former event does not provide a source instance or stack trace. This makes it near-impossible to track the source of warnings or info messages.

Some example use cases here could be:

  • Providing more context to AnalyticsService.FireLogEvent for non-error messages.
  • External output tracking & analytics with source context for services such as Sentry.
  • A custom in-game output console where only certain people (ex. developers) can see warning messages with stack traces, while others (ex. moderators) see them without stack traces to hide sensitive source code.

Roblox already provides developers with the necessary information from ScriptContext.Error mentioned above, but it is only usable for errors. Something similar for other types of messages would be great.

8 Likes

When I used pcall to catch errors I’ll usually send them to the output with a warn() call so my code can continue running. This makes debugging kind of difficult because I don’t get a trace of where the original error originated. This is another use case for what you’re describing.
I think it would be nice if pcall returned a third value on top of the success Boolean and errormessage string it already returns when it catches an error. This new value would also be a string but would contain the trace so it could be printed and logged separately.

1 Like

You might be interested in debug.traceback.

2 Likes

While this looks handy, I’m not sure it’s what I’m looking for. It looks like it generates a traceback for the line debug.traceback is called on so it can’t be used to find the origin of an error caught by pcall.

Take this example:

function a()
    if math.random(0, 1) == 1 then
        error("error!")
    else
        error("error!")
    end
end
function b()
    local success, err = pcall(a)
    if not success then warn("an error was thrown, but from where?", err) end
    -- <really important code that needs to still run, even if a() fails>
end

b()

how do I tell which error() call is the one generating the failure? Calling debug.traceback wouldn’t really help because while in this example I’m calling error(), what I’m more interested in catching is real errors. Errors where I can’t just call debug.traceback on the line before the error because I don’t know where the error is. If I didn’t call function “a” in a pcall then I would get an error with a traceback but b would also break. Calling function “a” within a pcall means b will continue to run but I won’t get a traceback for the error within “a”.

With the above example you get the following output where the line the error occurred on is mentioned in the err string:
an error was thrown, but from where? Workspace.Script:5: error!
This doesn’t work in all situations though. Some errors (ones not caused by error() or syntax errors, like teleportservice errors) don’t have what line caused them in the title. I’ll just get an error like “failed to teleport players because one or more players doesn’t have a parent” or something but I have a million different lines that could be coming from and it doesn’t say in the “err” string what line it came from.

Here’s a better example:

function c()
	game:GetService("TeleportService"):TeleportAsync(233195390, {})
end
function d()
	c()
	-- <very important code that must run, even if c() fails>
end

d()

Function “c” when called outright like this will give us an error like so:

20:57:55.502 Invalid list of players for teleport. - Server - Script:2
20:57:55.502 Stack Begin - Studio
20:57:55.502 Script ‘Workspace.Script’, Line 2 - function c - Studio - Script:2
20:57:55.502 Script ‘Workspace.Script’, Line 5 - function d - Studio - Script:5
20:57:55.502 Script ‘Workspace.Script’, Line 9 - Studio - Script:9
20:57:55.503 Stack End - Studio

This makes what line the error occurred on very clear but it also means that function d() will break on the line where it calls c() and the line after c() is called will never run. If we re-write function d() to include a pcall like so…

function d()
	local success, err = pcall(c)
	if not success then
		warn(err)
	end
	-- <very important code that must run, even if c() fails>
end

…function d() will get to finish, even if c() throws an error, but we won’t get any details about the error itself. This is what we will get in the output:

20:53:12.770 Invalid list of players for teleport. - Server - Script:7

Script:7 just points to the line warn() was called on, not where the actual error occurred within function c().

This is what I mean by pcall returning more values.
As things are, pcall returns the following:

1): A “success” boolean that will be true if function d() finished without an error, and will be false if function d() threw an error.
2a): If “success” is true, pcall will return all values returned by function d() (in this case none)
2b): If “success” is false, pcall will return a string containing the title of the error, but not the traceback.

What I propose is pcall should return a third value as so:

3): If “success” is false, pcall will return another string containing the traceback of the error.

How this would look in action:

function c()
	game:GetService("TeleportService"):TeleportAsync(233195390, {})
end
function d()
	local success, err, trace = pcall(c)
	if not success then
		warn(err, traceback)
	end
	-- <very important code that must run, even if c() fails>
end

d()

Output:

20:57:55.502 Invalid list of players for teleport. - Server - Script:2
20:57:55.502 Stack Begin - Studio
20:57:55.502 Script ‘Workspace.Script’, Line 2 - function c - Studio - Script:2
20:57:55.502 Script ‘Workspace.Script’, Line 5 - function d - Studio - Script:5
20:57:55.502 Script ‘Workspace.Script’, Line 9 - Studio - Script:9
20:57:55.503 Stack End - Studio

This way I could have the best of both worlds. I could allow function d() to complete without stopping for c()'s error, but still get the full details about c()'s error for debugging.

Hope this clears up what I meant. Sorry for any confusion.

2 Likes